Decompiled source of StoryKit v0.1.12

plugins/StoryKit.Core.dll

Decompiled 2 weeks ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Xml;
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("StoryKit.Core")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.12.0")]
[assembly: AssemblyInformationalVersion("0.1.12+de8ede7c1cc4e525b56eb73ca1c0bdd9555d1cc3")]
[assembly: AssemblyProduct("StoryKit.Core")]
[assembly: AssemblyTitle("StoryKit.Core")]
[assembly: AssemblyMetadata("BuildStamp", "de8ede7c 2026-09-05")]
[assembly: AssemblyVersion("0.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 StoryKit.Core
{
	public enum BusyLeg
	{
		None,
		Tree,
		Roster,
		ShopLocal,
		ShopRemote
	}
	public static class BusyLegNames
	{
		public static string Of(BusyLeg leg)
		{
			return leg switch
			{
				BusyLeg.Tree => "tree (NPCInteraction.ExtTree.isRunning)", 
				BusyLeg.Roster => "roster (NPCInteraction.m_npcInConversation)", 
				BusyLeg.ShopLocal => "shop (Merchant.Buyer + the local Shop panel is up)", 
				BusyLeg.ShopRemote => "shop (Merchant.Buyer is a remote shopper, inside the trust window)", 
				_ => "none", 
			};
		}
	}
	public static class ShopBusyRules
	{
		public static BusyLeg Judge(bool hasBuyer, bool buyerIsLocal, bool localPanelUp, bool transactionPending, float ageSeconds, float localGraceSeconds, float remoteGraceSeconds)
		{
			if (transactionPending)
			{
				if (!buyerIsLocal)
				{
					return BusyLeg.ShopRemote;
				}
				return BusyLeg.ShopLocal;
			}
			if (!hasBuyer)
			{
				return BusyLeg.None;
			}
			if (!buyerIsLocal)
			{
				if (!(ageSeconds <= remoteGraceSeconds))
				{
					return BusyLeg.None;
				}
				return BusyLeg.ShopRemote;
			}
			if (ageSeconds <= localGraceSeconds)
			{
				return BusyLeg.ShopLocal;
			}
			if (!localPanelUp)
			{
				return BusyLeg.None;
			}
			return BusyLeg.ShopLocal;
		}
	}
	public enum DespawnBusyVerdict
	{
		NotLive,
		RefuseBusy,
		Proceed
	}
	public static class DespawnBusyRules
	{
		public static DespawnBusyVerdict Judge(bool live, bool inDialogue, bool evenIfBusy)
		{
			if (!live)
			{
				return DespawnBusyVerdict.NotLive;
			}
			if (inDialogue && !evenIfBusy)
			{
				return DespawnBusyVerdict.RefuseBusy;
			}
			return DespawnBusyVerdict.Proceed;
		}

		public static string RefusalLine(string op, string id)
		{
			return RefusalLine(op, id, BusyLeg.None);
		}

		public static string RefusalLine(string op, string id, BusyLeg leg)
		{
			string text = ((leg == BusyLeg.None) ? "" : (" [busy leg: " + BusyLegNames.Of(leg) + "]"));
			return "[STORYKIT] " + op + "('" + id + "') REFUSED — in dialogue/trading" + text + "; destroying a Character under a running DialogueTree is the merchant-greet bug. Retry later: the guard re-evaluates fresh on every call. (Known gap: a guest's conversation with a spec that has no ActorLocKey — DR-minted on a guest — reads not-busy here, AO4-1.)";
		}
	}
	public enum DespawnVerdict
	{
		Clean,
		ForceClean,
		ReplacedByNewBody
	}
	public static class DespawnRules
	{
		public const float VerifyDelaySeconds = 0.3f;

		public static DespawnVerdict Judge(bool stillRegistered, bool sameObject)
		{
			if (!stillRegistered)
			{
				return DespawnVerdict.Clean;
			}
			if (!sameObject)
			{
				return DespawnVerdict.ReplacedByNewBody;
			}
			return DespawnVerdict.ForceClean;
		}
	}
	public static class DialogueHold
	{
		public const float MinPlanarDistance = 0.05f;

		public static bool ShouldHold(bool holdStillEnabled, bool conversationActive)
		{
			return holdStillEnabled && conversationActive;
		}

		public static bool TryFaceYaw(float npcX, float npcZ, float targetX, float targetZ, out float yawDegrees)
		{
			yawDegrees = 0f;
			float num = targetX - npcX;
			float num2 = targetZ - npcZ;
			if (num * num + num2 * num2 < 0.0025000002f)
			{
				return false;
			}
			float num3 = (float)(Math.Atan2(num, num2) * (180.0 / Math.PI));
			if (num3 < 0f)
			{
				num3 += 360f;
			}
			yawDegrees = num3;
			return true;
		}

		public static float ShortestTurn(float currentYaw, float targetYaw)
		{
			float num = (targetYaw - currentYaw) % 360f;
			if (num < -180f)
			{
				num += 360f;
			}
			else if (num > 180f)
			{
				num -= 360f;
			}
			return num;
		}
	}
	public static class DialogueLockRules
	{
		public static string KeyFor(string specId)
		{
			if (string.IsNullOrEmpty(specId))
			{
				return "";
			}
			return specId.Trim();
		}

		public static bool CanLock(string actorLocKey)
		{
			return !string.IsNullOrEmpty(actorLocKey);
		}

		public static bool NeedsLocEntry(string actorLocKey, string displayName, bool tableAlreadyHasKey)
		{
			if (!CanLock(actorLocKey))
			{
				return false;
			}
			if (tableAlreadyHasKey)
			{
				return false;
			}
			return !string.IsNullOrEmpty(displayName);
		}
	}
	public static class DialogueWire
	{
		public const string ChannelId = "story";

		public const string ChannelVersion = "0.1.0";

		public const string VerbRevision = "story.dlg";

		public const string VerbRevisionClear = "story.dlg.clear";

		public const string VerbInvoke = "story.act";

		internal const char Sep = '\t';

		public static bool IsFieldSafe(string s)
		{
			if (s != null && s.IndexOf('\t') < 0 && s.IndexOf('\n') < 0)
			{
				return s.IndexOf('\r') < 0;
			}
			return false;
		}
	}
	public struct RevisionStamp : IEquatable<RevisionStamp>
	{
		public readonly string Epoch;

		public readonly int Revision;

		public RevisionStamp(string epoch, int revision)
		{
			Epoch = epoch ?? "";
			Revision = revision;
		}

		public bool Equals(RevisionStamp other)
		{
			if (string.Equals(Epoch, other.Epoch, StringComparison.Ordinal))
			{
				return Revision == other.Revision;
			}
			return false;
		}

		public override bool Equals(object obj)
		{
			if (obj is RevisionStamp)
			{
				return Equals((RevisionStamp)obj);
			}
			return false;
		}

		public override int GetHashCode()
		{
			return ((Epoch ?? "").GetHashCode() * 397) ^ Revision;
		}

		public override string ToString()
		{
			string epoch = Epoch;
			int revision = Revision;
			return epoch + "#" + revision;
		}
	}
	public enum RevisionObservation
	{
		First,
		Same,
		Newer,
		Stale,
		NewEpoch
	}
	public sealed class RevisionBook
	{
		private readonly Dictionary<string, RevisionStamp> _known = new Dictionary<string, RevisionStamp>(StringComparer.Ordinal);

		public int Count => _known.Count;

		public RevisionObservation Observe(string specId, RevisionStamp stamp)
		{
			if (specId == null)
			{
				specId = "";
			}
			if (!_known.TryGetValue(specId, out var value))
			{
				_known[specId] = stamp;
				return RevisionObservation.First;
			}
			if (!string.Equals(value.Epoch, stamp.Epoch, StringComparison.Ordinal))
			{
				_known[specId] = stamp;
				return RevisionObservation.NewEpoch;
			}
			if (stamp.Revision == value.Revision)
			{
				return RevisionObservation.Same;
			}
			if (stamp.Revision < value.Revision)
			{
				return RevisionObservation.Stale;
			}
			_known[specId] = stamp;
			return RevisionObservation.Newer;
		}

		public bool TryGetKnown(string specId, out RevisionStamp stamp)
		{
			return _known.TryGetValue(specId ?? "", out stamp);
		}

		public bool Forget(string specId)
		{
			return _known.Remove(specId ?? "");
		}

		public void Clear()
		{
			_known.Clear();
		}
	}
	public static class RevisionCodec
	{
		public static string Encode(RevisionStamp stamp, string menuHint)
		{
			menuHint = menuHint ?? "";
			if (stamp.Revision < 1)
			{
				return null;
			}
			if (string.IsNullOrEmpty(stamp.Epoch))
			{
				return null;
			}
			if (!DialogueWire.IsFieldSafe(stamp.Epoch) || !DialogueWire.IsFieldSafe(menuHint))
			{
				return null;
			}
			string[] obj = new string[5] { stamp.Epoch, "\t", null, null, null };
			int revision = stamp.Revision;
			obj[2] = revision.ToString();
			obj[3] = "\t";
			obj[4] = menuHint;
			return string.Concat(obj);
		}

		public static bool TryParse(string payload, out RevisionStamp stamp, out string menuHint)
		{
			stamp = default(RevisionStamp);
			menuHint = "";
			if (string.IsNullOrEmpty(payload))
			{
				return false;
			}
			string[] array = payload.Split(new char[1] { '\t' });
			if (array.Length != 3)
			{
				return false;
			}
			if (string.IsNullOrEmpty(array[0]))
			{
				return false;
			}
			if (!int.TryParse(array[1], out var result) || result < 1)
			{
				return false;
			}
			stamp = new RevisionStamp(array[0], result);
			menuHint = array[2];
			return true;
		}
	}
	public static class InvokeCodec
	{
		private const string Ok = "ok";

		private const string Refused = "refused";

		public static string EncodeRequest(string specId, string actionId, RevisionStamp knownRev, string instigatorUid)
		{
			instigatorUid = instigatorUid ?? "";
			string text = knownRev.Epoch ?? "";
			if (string.IsNullOrEmpty(specId) || string.IsNullOrEmpty(actionId))
			{
				return null;
			}
			if (!DialogueWire.IsFieldSafe(specId) || !DialogueWire.IsFieldSafe(actionId) || !DialogueWire.IsFieldSafe(text) || !DialogueWire.IsFieldSafe(instigatorUid))
			{
				return null;
			}
			if (knownRev.Revision < 0)
			{
				return null;
			}
			string[] obj = new string[9] { specId, "\t", actionId, "\t", text, "\t", null, null, null };
			int revision = knownRev.Revision;
			obj[6] = revision.ToString();
			obj[7] = "\t";
			obj[8] = instigatorUid;
			return string.Concat(obj);
		}

		public static bool TryParseRequest(string body, out string specId, out string actionId, out RevisionStamp knownRev, out string instigatorUid)
		{
			specId = (actionId = (instigatorUid = ""));
			knownRev = default(RevisionStamp);
			if (string.IsNullOrEmpty(body))
			{
				return false;
			}
			string[] array = body.Split(new char[1] { '\t' });
			if (array.Length != 5)
			{
				return false;
			}
			if (string.IsNullOrEmpty(array[0]) || string.IsNullOrEmpty(array[1]))
			{
				return false;
			}
			if (!int.TryParse(array[3], out var result) || result < 0)
			{
				return false;
			}
			specId = array[0];
			actionId = array[1];
			knownRev = new RevisionStamp(array[2], result);
			instigatorUid = array[4];
			return true;
		}

		public static string EncodeOk(string detail = "")
		{
			detail = detail ?? "";
			if (!DialogueWire.IsFieldSafe(detail))
			{
				return null;
			}
			if (detail.Length != 0)
			{
				return "ok\t" + detail;
			}
			return "ok";
		}

		public static string EncodeRefusal(string reason, string detail = "")
		{
			detail = detail ?? "";
			if (string.IsNullOrEmpty(reason))
			{
				return null;
			}
			if (!DialogueWire.IsFieldSafe(reason) || !DialogueWire.IsFieldSafe(detail))
			{
				return null;
			}
			if (detail.Length != 0)
			{
				return "refused\t" + reason + "\t" + detail;
			}
			return "refused\t" + reason;
		}

		public static bool ParseResult(string result, out string refusalReason, out string detail)
		{
			refusalReason = "";
			detail = "";
			if (string.IsNullOrEmpty(result))
			{
				refusalReason = "malformed";
				return false;
			}
			string[] array = result.Split(new char[1] { '\t' });
			if (array[0] == "ok" && array.Length <= 2)
			{
				if (array.Length == 2)
				{
					detail = array[1];
				}
				return true;
			}
			if (array[0] == "refused" && (array.Length == 2 || array.Length == 3) && array[1].Length != 0)
			{
				refusalReason = array[1];
				if (array.Length == 3)
				{
					detail = array[2];
				}
				return false;
			}
			refusalReason = "malformed";
			return false;
		}
	}
	public static class InvokeRefusal
	{
		public const string NoSpec = "no-spec";

		public const string NoHandler = "no-handler";

		public const string StaleRevision = "stale-rev";

		public const string NoInstigator = "no-instigator";

		public const string NotMaster = "not-master";

		public const string HandlerRefused = "handler-refused";

		public const string HandlerThrew = "handler-threw";

		public const string Malformed = "malformed";

		public static bool ShouldRerender(string reason)
		{
			if (!(reason == "stale-rev"))
			{
				return reason == "no-spec";
			}
			return true;
		}

		public static string Describe(string reason)
		{
			return reason switch
			{
				"no-spec" => "the master no longer has this NPC", 
				"no-handler" => "the master has no handler for this choice", 
				"stale-rev" => "this menu is out of date — reopening", 
				"no-instigator" => "the master could not resolve who picked this", 
				"not-master" => "the session master changed mid-request", 
				"handler-refused" => "the master declined this choice", 
				"handler-threw" => "the master failed running this choice", 
				"malformed" => "the answer could not be read", 
				_ => "refused (" + reason + ")", 
			};
		}
	}
	public static class JsonText
	{
		public static string Quote(string s)
		{
			if (s == null)
			{
				return "\"\"";
			}
			StringBuilder stringBuilder = new StringBuilder(s.Length + 2);
			stringBuilder.Append('"');
			foreach (char c in s)
			{
				if (c == '"' || c == '\\')
				{
					stringBuilder.Append('\\').Append(c);
				}
				else if (c == '\n')
				{
					stringBuilder.Append("\\n");
				}
				else if (c == '\r')
				{
					stringBuilder.Append("\\r");
				}
				else if (c == '\t')
				{
					stringBuilder.Append("\\t");
				}
				else if (c == '\b')
				{
					stringBuilder.Append("\\b");
				}
				else if (c == '\f')
				{
					stringBuilder.Append("\\f");
				}
				else if (c < ' ')
				{
					StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
					int num = c;
					stringBuilder2.Append(num.ToString("x4"));
				}
				else
				{
					stringBuilder.Append(c);
				}
			}
			return stringBuilder.Append('"').ToString();
		}
	}
	public enum MenuNodeKind
	{
		Greet,
		Train,
		Menu,
		Reply,
		Shop,
		Action
	}
	public struct MenuNodeRef : IEquatable<MenuNodeRef>
	{
		public readonly MenuNodeKind Kind;

		public readonly int Id;

		public static readonly MenuNodeRef Greet = new MenuNodeRef(MenuNodeKind.Greet, -1);

		public static readonly MenuNodeRef Train = new MenuNodeRef(MenuNodeKind.Train, -1);

		public static readonly MenuNodeRef Shop = new MenuNodeRef(MenuNodeKind.Shop, -1);

		public MenuNodeRef(MenuNodeKind kind, int id)
		{
			Kind = kind;
			Id = id;
		}

		public bool Equals(MenuNodeRef other)
		{
			if (Kind == other.Kind)
			{
				return Id == other.Id;
			}
			return false;
		}

		public override bool Equals(object obj)
		{
			if (obj is MenuNodeRef)
			{
				return Equals((MenuNodeRef)obj);
			}
			return false;
		}

		public override int GetHashCode()
		{
			return ((int)Kind * 397) ^ Id;
		}

		public override string ToString()
		{
			if (Id < 0)
			{
				return Kind.ToString();
			}
			string text = Kind.ToString();
			int id = Id;
			return text + "#" + id;
		}
	}
	public sealed class MenuNode
	{
		public MenuNodeKind Kind;

		public int Id;

		public int Depth;

		public string ChoiceId;

		public List<string> Labels = new List<string>();

		public int BackIndex = -1;

		public string ReplyText;

		public string ActionId;

		public MenuNodeRef Ref => new MenuNodeRef(Kind, Id);
	}
	public sealed class MenuEdge
	{
		public MenuNodeRef Source;

		public int SourceIndex;

		public MenuNodeRef Target;

		public string Label;

		public override string ToString()
		{
			string[] array = new string[8];
			MenuNodeRef source = Source;
			array[0] = source.ToString();
			array[1] = "[";
			array[2] = SourceIndex.ToString();
			array[3] = "] -> ";
			source = Target;
			array[4] = source.ToString();
			array[5] = " '";
			array[6] = Label;
			array[7] = "'";
			return string.Concat(array);
		}
	}
	public enum MenuDropReason
	{
		NullRow,
		TrainNoTrainer,
		TrainInSubmenu,
		TrainNoNode,
		ShopNoMerchant,
		ShopInSubmenu,
		ActionNoHandler
	}
	public sealed class MenuDrop
	{
		public MenuDropReason Reason;

		public int Depth;

		public int Index;

		public string ChoiceId;

		public override string ToString()
		{
			return Reason.ToString() + " depth=" + Depth + " #" + Index + ((ChoiceId != null) ? (" '" + ChoiceId + "'") : "");
		}
	}
	public sealed class MenuPlan
	{
		public List<MenuNode> Nodes = new List<MenuNode>();

		public List<MenuEdge> Edges = new List<MenuEdge>();

		public MenuNodeRef Root;

		public string Outline = "";

		public int NullRowsSkipped;

		public List<MenuDrop> Drops = new List<MenuDrop>();

		public int VisibleRowCount(MenuNodeRef r)
		{
			MenuNode menuNode = Node(r);
			if (menuNode == null)
			{
				return 0;
			}
			return menuNode.Labels.Count - ((menuNode.BackIndex >= 0) ? 1 : 0);
		}

		public MenuNode Node(MenuNodeRef r)
		{
			if (r.Id < 0)
			{
				return null;
			}
			return Nodes[r.Id];
		}
	}
	public static class MenuPlanner
	{
		private sealed class PlanFlags
		{
			public bool WantsTrain;

			public bool TrainNodeAvailable;

			public bool WantsShop;

			public Func<string, bool> HandlerAvailable;
		}

		public static MenuPlan Plan(IList<Choice> rows, bool wantsTrain, bool trainNodeAvailable, string rootBackText = null)
		{
			return Plan(rows, wantsTrain, trainNodeAvailable, rootBackText, wantsShop: false, shopNodeAvailable: false);
		}

		public static MenuPlan Plan(IList<Choice> rows, bool wantsTrain, bool trainNodeAvailable, string rootBackText, bool wantsShop, bool shopNodeAvailable)
		{
			return Plan(rows, wantsTrain, trainNodeAvailable, rootBackText, wantsShop, shopNodeAvailable, null);
		}

		public static MenuPlan Plan(IList<Choice> rows, bool wantsTrain, bool trainNodeAvailable, string rootBackText, bool wantsShop, bool shopNodeAvailable, Func<string, bool> handlerAvailable)
		{
			MenuPlan menuPlan = new MenuPlan();
			StringBuilder stringBuilder = new StringBuilder();
			PlanFlags flags = new PlanFlags
			{
				WantsTrain = wantsTrain,
				TrainNodeAvailable = trainNodeAvailable,
				WantsShop = (wantsShop && shopNodeAvailable),
				HandlerAvailable = handlerAvailable
			};
			menuPlan.Root = PlanMenu(menuPlan, stringBuilder, flags, rows, MenuNodeRef.Greet, rootBackText, 0);
			menuPlan.Outline = stringBuilder.ToString();
			return menuPlan;
		}

		private static MenuNodeRef PlanMenu(MenuPlan plan, StringBuilder outline, PlanFlags flags, IList<Choice> rows, MenuNodeRef returnTarget, string backText, int depth)
		{
			if (depth > 4)
			{
				throw new InvalidOperationException("submenu nesting exceeded MaxMenuDepth (" + 3 + ") at depth " + depth + " — a validation bypass or a cycle in the Choice graph.");
			}
			List<Choice> list = new List<Choice>();
			if (rows != null)
			{
				for (int i = 0; i < rows.Count; i++)
				{
					Choice choice = rows[i];
					if (choice == null)
					{
						plan.NullRowsSkipped++;
						plan.Drops.Add(new MenuDrop
						{
							Reason = MenuDropReason.NullRow,
							Depth = depth,
							Index = i
						});
					}
					else if (choice.Kind == ChoiceKind.Train && (!flags.WantsTrain || depth > 0 || !flags.TrainNodeAvailable))
					{
						plan.Drops.Add(new MenuDrop
						{
							Reason = ((depth > 0) ? MenuDropReason.TrainInSubmenu : ((!flags.WantsTrain) ? MenuDropReason.TrainNoTrainer : MenuDropReason.TrainNoNode)),
							Depth = depth,
							Index = i,
							ChoiceId = choice.Id
						});
					}
					else if (choice.Kind == ChoiceKind.Shop && (!flags.WantsShop || depth > 0))
					{
						plan.Drops.Add(new MenuDrop
						{
							Reason = ((depth > 0) ? MenuDropReason.ShopInSubmenu : MenuDropReason.ShopNoMerchant),
							Depth = depth,
							Index = i,
							ChoiceId = choice.Id
						});
					}
					else if (choice.Kind == ChoiceKind.Action && (string.IsNullOrEmpty(choice.ActionId) || flags.HandlerAvailable == null || !flags.HandlerAvailable(choice.ActionId)))
					{
						plan.Drops.Add(new MenuDrop
						{
							Reason = MenuDropReason.ActionNoHandler,
							Depth = depth,
							Index = i,
							ChoiceId = choice.Id
						});
					}
					else
					{
						list.Add(choice);
					}
				}
			}
			MenuNode menuNode = new MenuNode
			{
				Kind = MenuNodeKind.Menu,
				Id = plan.Nodes.Count,
				Depth = depth
			};
			plan.Nodes.Add(menuNode);
			string text = new string(' ', 2 + depth * 2);
			for (int j = 0; j < list.Count; j++)
			{
				Choice choice2 = list[j];
				menuNode.Labels.Add(choice2.Text);
				outline.Append("\n" + text + j + ". '" + Trim(choice2.Text) + "' → " + choice2.Kind);
				if (choice2.Kind == ChoiceKind.Train)
				{
					plan.Edges.Add(new MenuEdge
					{
						Source = menuNode.Ref,
						SourceIndex = j,
						Target = MenuNodeRef.Train,
						Label = choice2.Id
					});
					continue;
				}
				if (choice2.Kind == ChoiceKind.Shop)
				{
					plan.Edges.Add(new MenuEdge
					{
						Source = menuNode.Ref,
						SourceIndex = j,
						Target = MenuNodeRef.Shop,
						Label = choice2.Id
					});
					continue;
				}
				if (choice2.Kind == ChoiceKind.Menu)
				{
					MenuNodeRef target = PlanMenu(plan, outline, flags, choice2.Children, menuNode.Ref, choice2.BackText ?? "← Back", depth + 1);
					plan.Edges.Add(new MenuEdge
					{
						Source = menuNode.Ref,
						SourceIndex = j,
						Target = target,
						Label = choice2.Id
					});
					continue;
				}
				MenuNodeRef source = menuNode.Ref;
				int sourceIndex = j;
				if (choice2.Kind == ChoiceKind.Action)
				{
					MenuNode menuNode2 = new MenuNode
					{
						Kind = MenuNodeKind.Action,
						Id = plan.Nodes.Count,
						Depth = depth,
						ChoiceId = choice2.Id,
						ActionId = choice2.ActionId
					};
					plan.Nodes.Add(menuNode2);
					plan.Edges.Add(new MenuEdge
					{
						Source = menuNode.Ref,
						SourceIndex = j,
						Target = menuNode2.Ref,
						Label = choice2.Id
					});
					source = menuNode2.Ref;
					sourceIndex = -1;
				}
				MenuNode menuNode3 = new MenuNode
				{
					Kind = MenuNodeKind.Reply,
					Id = plan.Nodes.Count,
					Depth = depth,
					ChoiceId = choice2.Id,
					ReplyText = (choice2.ReplyText ?? "...")
				};
				plan.Nodes.Add(menuNode3);
				plan.Edges.Add(new MenuEdge
				{
					Source = source,
					SourceIndex = sourceIndex,
					Target = menuNode3.Ref,
					Label = choice2.Id
				});
				plan.Edges.Add(new MenuEdge
				{
					Source = menuNode3.Ref,
					Target = ((depth == 0) ? MenuNodeRef.Greet : menuNode.Ref),
					SourceIndex = -1,
					Label = choice2.Id + ":return"
				});
			}
			if (backText != null)
			{
				menuNode.BackIndex = list.Count;
				menuNode.Labels.Add(backText);
				plan.Edges.Add(new MenuEdge
				{
					Source = menuNode.Ref,
					Target = returnTarget,
					SourceIndex = list.Count,
					Label = "<back>"
				});
				outline.Append("\n" + text + list.Count + ". '" + Trim(backText) + "' → Back");
			}
			return menuNode.Ref;
		}

		internal static string Trim(string s)
		{
			if (s == null || s.Length <= 30)
			{
				return s;
			}
			return s.Substring(0, 30) + "…";
		}
	}
	public static class MerchantRefreshRules
	{
		public const double Unstamped = -1.0;

		public static bool ShouldFillFallback(double nextRefreshBefore, double nextRefreshAfter, int itemCount)
		{
			if (itemCount > 0)
			{
				return false;
			}
			return nextRefreshAfter > nextRefreshBefore;
		}

		public static double StampAfterFallback(double gameTime, float specRefreshRateGameHours, double nextRefreshAfterRoll)
		{
			double num = ((specRefreshRateGameHours > 0f) ? ((double)specRefreshRateGameHours) : 0.0);
			double num2 = gameTime + num;
			if (!(nextRefreshAfterRoll > num2))
			{
				return num2;
			}
			return nextRefreshAfterRoll;
		}
	}
	public enum RoomRoleEvent
	{
		First,
		None,
		RoomChanged,
		MasterFlipped
	}
	public sealed class RoomRoleWatch
	{
		private string _room;

		private bool _isMaster;

		private bool _known;

		public string Room => _room;

		public bool IsMaster => _isMaster;

		public RoomRoleEvent Observe(string room, bool isMaster)
		{
			bool flag = _known && !string.Equals(room, _room, StringComparison.Ordinal);
			bool flag2 = _known && isMaster != _isMaster;
			bool num = !_known;
			_room = room;
			_isMaster = isMaster;
			_known = true;
			if (num)
			{
				return RoomRoleEvent.First;
			}
			if (flag)
			{
				return RoomRoleEvent.RoomChanged;
			}
			if (flag2)
			{
				return RoomRoleEvent.MasterFlipped;
			}
			return RoomRoleEvent.None;
		}
	}
	public static class OrphanSweepRules
	{
		public static bool LooksMintedSpecId(string uid)
		{
			if (string.IsNullOrEmpty(uid))
			{
				return false;
			}
			if (uid.Length >= 20)
			{
				return uid.IndexOf('.') >= 0;
			}
			return true;
		}

		public static bool ShouldSweep(bool slNamed, bool hasLocalTemplate, bool hasLocalSpec, string uid)
		{
			if (slNamed && !hasLocalTemplate && !hasLocalSpec)
			{
				return LooksMintedSpecId(uid);
			}
			return false;
		}
	}
	public sealed class NpcSpec
	{
		public const float WalkSpeed = 0.3f;

		public const float RunSpeed = 1.1f;

		public string Id;

		public string Name;

		public List<Placement> Placements = new List<Placement>();

		public TrainerSpec Trainer;

		public DialogueSpec Dialogue = new DialogueSpec();

		public int WeaponId;

		public int HelmetId;

		public int ChestId;

		public int BootsId;

		public int BackpackId;

		public int ShieldId;

		public VisualIndices Visuals;

		public bool RandomVisuals;

		public List<OutfitSpec> OutfitPool;

		public bool LookFollowEnabled = true;

		public bool HoldStillInDialogue = true;

		public bool Mobile;

		public AiSpec Ai;

		public CombatSpec Combat;

		public string BackpackName;

		public string Faction;

		public MerchantSpec Merchant;
	}
	public sealed class VisualIndices
	{
		public int Gender;

		public int SkinIndex;

		public int HeadVariationIndex;

		public int HairStyleIndex;

		public int HairColorIndex;

		public override string ToString()
		{
			return $"gender={Gender} skin={SkinIndex} head={HeadVariationIndex} hair={HairStyleIndex} hairColor={HairColorIndex}";
		}
	}
	public sealed class OutfitSpec
	{
		public string ChestName;

		public string HelmetName;

		public string BootsName;

		public string WeaponName;

		public bool IsEmpty
		{
			get
			{
				if (string.IsNullOrEmpty(ChestName) && string.IsNullOrEmpty(HelmetName) && string.IsNullOrEmpty(BootsName))
				{
					return string.IsNullOrEmpty(WeaponName);
				}
				return false;
			}
		}

		public OutfitSpec()
		{
		}

		public OutfitSpec(string chest, string helmet = null, string boots = null, string weapon = null)
		{
			ChestName = chest;
			HelmetName = helmet;
			BootsName = boots;
			WeaponName = weapon;
		}
	}
	public sealed class AiSpec
	{
		public float WanderSpeed = 0.3f;

		public bool CanWanderFar = true;

		public bool CanBlock = true;

		public bool CanDodge = true;

		public float ChanceToAttack = 75f;

		public bool Passive;
	}
	public sealed class CombatSpec
	{
		public float? Health;

		public float? Protection;

		public float[] DamageResists;

		public float DamageBonusMult = 1f;

		public List<string> TargetableFactions;
	}
	public sealed class MerchantSpec
	{
		public string StockTableNameContains;

		public List<string> FallbackItemNames;

		public float RefreshRateGameHours = 72f;

		public bool NonSavable = true;

		public bool Buyer = true;

		public bool Seller = true;
	}
	public sealed class Placement
	{
		public string Scene;

		public float X;

		public float Y;

		public float Z;

		public float RotationY;

		public Placement()
		{
		}

		public Placement(string scene, float x, float y, float z, float rotY = 0f)
		{
			Scene = scene;
			X = x;
			Y = y;
			Z = z;
			RotationY = rotY;
		}
	}
	public sealed class TrainerSpec
	{
		public string SkillTreeUID;

		public SkillTreeDef Tree;
	}
	public sealed class DialogueSpec
	{
		public List<string> Greetings = new List<string>();

		public List<Choice> Choices = new List<Choice>();
	}
	public enum ChoiceKind
	{
		Train,
		Reply,
		Menu,
		Shop,
		Action
	}
	public sealed class Choice
	{
		public string Id;

		public string Text;

		public ChoiceKind Kind;

		public string ReplyText;

		public string ActionId;

		public List<Choice> Children;

		public string BackText = "← Back";

		public static Choice Train(string id, string text)
		{
			return new Choice
			{
				Id = id,
				Text = text,
				Kind = ChoiceKind.Train
			};
		}

		public static Choice Reply(string id, string text, string replyText)
		{
			return new Choice
			{
				Id = id,
				Text = text,
				Kind = ChoiceKind.Reply,
				ReplyText = replyText
			};
		}

		public static Choice Action(string id, string text, string replyText, string actionId)
		{
			return new Choice
			{
				Id = id,
				Text = text,
				Kind = ChoiceKind.Action,
				ReplyText = replyText,
				ActionId = actionId
			};
		}

		public static Choice Shop(string id, string text)
		{
			return new Choice
			{
				Id = id,
				Text = text,
				Kind = ChoiceKind.Shop
			};
		}

		public static Choice Menu(string id, string text, params Choice[] children)
		{
			return new Choice
			{
				Id = id,
				Text = text,
				Kind = ChoiceKind.Menu,
				Children = new List<Choice>(children ?? new Choice[0])
			};
		}
	}
	public sealed class SkillTreeDef
	{
		public string Name;

		public List<SlotDef> Slots = new List<SlotDef>();
	}
	public sealed class SlotDef
	{
		public int Row;

		public int Column;

		public int SkillId;

		public int SilverCost;

		public int RequiredRow;

		public int RequiredColumn;

		public bool Breakthrough;

		public SlotDef()
		{
		}

		public SlotDef(int row, int col, int skillId, int silverCost, int requiredRow = 0, int requiredColumn = 0, bool breakthrough = false)
		{
			Row = row;
			Column = col;
			SkillId = skillId;
			SilverCost = silverCost;
			RequiredRow = requiredRow;
			RequiredColumn = requiredColumn;
			Breakthrough = breakthrough;
		}
	}
	public static class OutfitRoll
	{
		public static OutfitSpec Pick(IList<OutfitSpec> pool, double roll01)
		{
			if (pool == null || pool.Count == 0)
			{
				return null;
			}
			if (roll01 < 0.0)
			{
				roll01 = 0.0;
			}
			if (roll01 > 1.0)
			{
				roll01 = 1.0;
			}
			int num = (int)(roll01 * (double)pool.Count);
			if (num >= pool.Count)
			{
				num = pool.Count - 1;
			}
			return pool[num];
		}
	}
	public static class QuestEventRules
	{
		public const string ReconUidPrefix = "bw.srecon.";

		public static bool IsPresent(int stack)
		{
			return stack > 0;
		}

		public static bool IsStored(int stack)
		{
			return stack >= 0;
		}

		public static string LivenessLabel(int stack)
		{
			if (stack >= 0)
			{
				if (stack <= 0)
				{
					return "DORMANT";
				}
				return "ACTIVE";
			}
			return null;
		}

		public static bool IsReconSaveRow(string identifier)
		{
			return identifier?.StartsWith("bw.srecon.", StringComparison.Ordinal) ?? false;
		}
	}
	public enum RegistryOp
	{
		SpawnAt,
		Despawn,
		Unregister,
		Respawn,
		Rebuild,
		ReplaceGreetings
	}
	public static class RegistryAuthority
	{
		public static bool Allowed(RegistryOp op, bool isNonMasterClient)
		{
			return !isNonMasterClient;
		}

		public static string Refusal(RegistryOp op, string id)
		{
			return $"[STORYKIT] {op}('{id}') REFUSED — non-master client; the master owns the NPC lifecycle (a guest call here would delete or fork the NPC for every peer).";
		}
	}
	public enum FailureVerdict
	{
		Report,
		Quiet,
		Capped
	}
	public sealed class RigFailurePolicy
	{
		public const int DefaultMaxAttempts = 5;

		private readonly Dictionary<string, int> _failures = new Dictionary<string, int>();

		public int MaxAttempts { get; }

		public IEnumerable<string> CappedIds
		{
			get
			{
				foreach (KeyValuePair<string, int> failure in _failures)
				{
					if (failure.Value >= MaxAttempts)
					{
						yield return failure.Key;
					}
				}
			}
		}

		public RigFailurePolicy()
			: this(5)
		{
		}

		public RigFailurePolicy(int maxAttempts)
		{
			MaxAttempts = ((maxAttempts < 1) ? 1 : maxAttempts);
		}

		private static string Key(string id)
		{
			return id ?? string.Empty;
		}

		public int FailureCount(string id)
		{
			if (!_failures.TryGetValue(Key(id), out var value))
			{
				return 0;
			}
			return value;
		}

		public bool ShouldAttempt(string id)
		{
			return FailureCount(id) < MaxAttempts;
		}

		public void RecordSuccess(string id)
		{
			_failures.Remove(Key(id));
		}

		public FailureVerdict RecordFailure(string id)
		{
			string text = Key(id);
			int num = FailureCount(text) + 1;
			if (num > MaxAttempts)
			{
				num = MaxAttempts;
			}
			_failures[text] = num;
			if (num >= MaxAttempts)
			{
				return FailureVerdict.Capped;
			}
			if (num != 1)
			{
				return FailureVerdict.Quiet;
			}
			return FailureVerdict.Report;
		}

		public void Reset()
		{
			_failures.Clear();
		}
	}
	public readonly struct RingSlot
	{
		public readonly float AngleDeg;

		public readonly float X;

		public readonly float Z;

		public RingSlot(float angleDeg, float x, float z)
		{
			AngleDeg = angleDeg;
			X = x;
			Z = z;
		}

		public static RingSlot At(float angleDeg, float radius)
		{
			double num = (double)angleDeg * Math.PI / 180.0;
			return new RingSlot(angleDeg, (float)(Math.Sin(num) * (double)radius), (float)(Math.Cos(num) * (double)radius));
		}

		public override string ToString()
		{
			return $"{AngleDeg:F0}° ({X:F2},{Z:F2})";
		}
	}
	public static class RingPlan
	{
		public static readonly float[] AngleNudgesDeg = new float[4] { 15f, -15f, 30f, -30f };

		public static readonly float[] RadiusScales = new float[2] { 0.75f, 1.25f };

		public static IReadOnlyList<RingSlot> Slots(int count, float radius, float centreAngleDeg, float spreadDeg)
		{
			List<RingSlot> list = new List<RingSlot>(Math.Max(0, count));
			if (count <= 0)
			{
				return list;
			}
			if (count == 1)
			{
				list.Add(RingSlot.At(centreAngleDeg, radius));
				return list;
			}
			float num = spreadDeg / (float)(count - 1);
			float num2 = centreAngleDeg - spreadDeg / 2f;
			for (int i = 0; i < count; i++)
			{
				list.Add(RingSlot.At(num2 + (float)i * num, radius));
			}
			return list;
		}

		public static IReadOnlyList<RingSlot> Fallbacks(RingSlot slot, float radius)
		{
			List<RingSlot> list = new List<RingSlot>(AngleNudgesDeg.Length + RadiusScales.Length * (AngleNudgesDeg.Length + 1));
			float[] angleNudgesDeg = AngleNudgesDeg;
			foreach (float num in angleNudgesDeg)
			{
				list.Add(RingSlot.At(slot.AngleDeg + num, radius));
			}
			angleNudgesDeg = RadiusScales;
			foreach (float num2 in angleNudgesDeg)
			{
				float radius2 = radius * num2;
				list.Add(RingSlot.At(slot.AngleDeg, radius2));
				float[] angleNudgesDeg2 = AngleNudgesDeg;
				foreach (float num3 in angleNudgesDeg2)
				{
					list.Add(RingSlot.At(slot.AngleDeg + num3, radius2));
				}
			}
			return list;
		}

		public static bool Spaced(RingSlot candidate, IReadOnlyList<RingSlot> placed, float minSpacing)
		{
			if (placed == null || minSpacing <= 0f)
			{
				return true;
			}
			float num = minSpacing * minSpacing;
			for (int i = 0; i < placed.Count; i++)
			{
				float num2 = candidate.X - placed[i].X;
				float num3 = candidate.Z - placed[i].Z;
				if (num2 * num2 + num3 * num3 < num)
				{
					return false;
				}
			}
			return true;
		}
	}
	public static class SaveArtifact
	{
		private static readonly string[] Markers = new string[2] { ".bak", ".removed-for-" };

		public static bool IsBackupUid(string uid)
		{
			return MarkerIn(uid) != null;
		}

		public static string MarkerIn(string uid)
		{
			if (string.IsNullOrEmpty(uid))
			{
				return null;
			}
			string[] markers = Markers;
			foreach (string text in markers)
			{
				if (uid.IndexOf(text, StringComparison.OrdinalIgnoreCase) >= 0)
				{
					return text;
				}
			}
			return null;
		}

		public static string OriginalUid(string uid)
		{
			string text = MarkerIn(uid);
			if (text == null)
			{
				return uid;
			}
			int num = uid.IndexOf(text, StringComparison.OrdinalIgnoreCase);
			if (num <= 0)
			{
				return uid;
			}
			return uid.Substring(0, num);
		}

		public static string ReasonFor(string uid)
		{
			string text = MarkerIn(uid);
			if (text == null)
			{
				return null;
			}
			string text2 = OriginalUid(uid);
			return "backup artifact ('" + text + "' in the save folder name)" + ((text2 != uid) ? (" — a copy of '" + text2 + "'") : "");
		}
	}
	public struct SavedItem
	{
		public string Uid;

		public int ItemId;

		public string Hierarchy;

		public int Quantity;

		public float Durability;

		public string SubClassesData;

		public string ItemExtensions;

		public override string ToString()
		{
			return $"{ItemId} x{Quantity} @'{Hierarchy}' ({Uid})";
		}
	}
	public static class SavedItemParser
	{
		public static bool TryParse(string syncDataXml, out SavedItem item)
		{
			item = default(SavedItem);
			if (string.IsNullOrEmpty(syncDataXml))
			{
				return false;
			}
			try
			{
				XmlDocument xmlDocument = new XmlDocument();
				xmlDocument.LoadXml(syncDataXml);
				XmlElement xmlElement = xmlDocument["Item"];
				if (xmlElement == null)
				{
					return false;
				}
				XmlElement xmlElement2 = xmlElement["ID"];
				if (xmlElement2 == null || !int.TryParse(xmlElement2.InnerText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
				{
					return false;
				}
				item = new SavedItem
				{
					Uid = Text(xmlElement["UID"]),
					ItemId = result,
					Hierarchy = Text(xmlElement["Hierarchy"]),
					Quantity = 1,
					Durability = -1f,
					SubClassesData = Text(xmlElement["SubClassesData"]),
					ItemExtensions = Text(xmlElement["ItemExtensions"])
				};
				XmlElement xmlElement3 = xmlElement["Quantity"];
				if (xmlElement3 != null && int.TryParse(xmlElement3.InnerText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2) && result2 > 0)
				{
					item.Quantity = result2;
				}
				XmlElement xmlElement4 = xmlElement["Durability"];
				if (xmlElement4 != null && float.TryParse(xmlElement4.InnerText, NumberStyles.Float, CultureInfo.InvariantCulture, out var result3))
				{
					item.Durability = result3;
				}
				return true;
			}
			catch (XmlException)
			{
				return false;
			}
		}

		private static string Text(XmlElement e)
		{
			if (e == null)
			{
				return "";
			}
			return e.InnerText ?? "";
		}

		public static string WornHierarchy(string ownerUid)
		{
			return "2" + ownerUid;
		}

		public static string PouchUid(string ownerUid)
		{
			return "Pouch_" + ownerUid;
		}

		public static string BagContentUid(string bagUid)
		{
			return bagUid + "_Content";
		}

		public static bool IsWornBy(SavedItem it, string ownerUid)
		{
			if (!string.IsNullOrEmpty(ownerUid))
			{
				return string.Equals(it.Hierarchy, WornHierarchy(ownerUid), StringComparison.Ordinal);
			}
			return false;
		}

		public static bool IsInsideContainer(SavedItem it, string containerUid)
		{
			if (!string.IsNullOrEmpty(containerUid) && it.Hierarchy != null)
			{
				return it.Hierarchy.StartsWith("1" + containerUid + ";", StringComparison.Ordinal);
			}
			return false;
		}

		public static bool IsInPouchOf(SavedItem it, string ownerUid)
		{
			if (!string.IsNullOrEmpty(ownerUid))
			{
				return IsInsideContainer(it, PouchUid(ownerUid));
			}
			return false;
		}

		public static bool IsInBag(SavedItem it, string bagUid)
		{
			if (!string.IsNullOrEmpty(bagUid))
			{
				return IsInsideContainer(it, BagContentUid(bagUid));
			}
			return false;
		}

		public static List<SavedItem> WornOf(string ownerUid, IEnumerable<SavedItem> items)
		{
			return Where(items, (SavedItem it) => IsWornBy(it, ownerUid) && it.ItemId > 0);
		}

		public static List<SavedItem> PouchOf(string ownerUid, IEnumerable<SavedItem> items)
		{
			return Where(items, (SavedItem it) => IsInPouchOf(it, ownerUid) && it.ItemId > 0);
		}

		public static List<SavedItem> BagContentsOf(string bagUid, IEnumerable<SavedItem> items)
		{
			return Where(items, (SavedItem it) => IsInBag(it, bagUid) && it.ItemId > 0);
		}

		public static List<int> EquippedItemIds(string ownerUid, IEnumerable<SavedItem> items)
		{
			List<int> list = new List<int>();
			if (string.IsNullOrEmpty(ownerUid) || items == null)
			{
				return list;
			}
			foreach (SavedItem item in items)
			{
				if (IsWornBy(item, ownerUid) && item.ItemId > 0)
				{
					list.Add(item.ItemId);
				}
			}
			return list;
		}

		public static List<KeyValuePair<int, List<int>>> EquippedItemEnchants(string ownerUid, IEnumerable<SavedItem> items)
		{
			List<KeyValuePair<int, List<int>>> list = new List<KeyValuePair<int, List<int>>>();
			if (string.IsNullOrEmpty(ownerUid) || items == null)
			{
				return list;
			}
			foreach (SavedItem item in items)
			{
				if (IsWornBy(item, ownerUid) && item.ItemId > 0)
				{
					list.Add(new KeyValuePair<int, List<int>>(item.ItemId, EnchantmentIds(item.SubClassesData)));
				}
			}
			return list;
		}

		public static string KnowledgeHierarchy(string ownerUid)
		{
			return "3" + ownerUid;
		}

		public static bool IsKnowledgeOf(SavedItem it, string ownerUid)
		{
			if (!string.IsNullOrEmpty(ownerUid))
			{
				return string.Equals(it.Hierarchy, KnowledgeHierarchy(ownerUid), StringComparison.Ordinal);
			}
			return false;
		}

		public static List<int> KnowledgeItemIds(string ownerUid, IEnumerable<SavedItem> items)
		{
			List<int> list = new List<int>();
			if (string.IsNullOrEmpty(ownerUid) || items == null)
			{
				return list;
			}
			foreach (SavedItem item in items)
			{
				if (IsKnowledgeOf(item, ownerUid) && item.ItemId > 0)
				{
					list.Add(item.ItemId);
				}
			}
			return list;
		}

		public static int QuickslotItemId(string saveData)
		{
			if (string.IsNullOrEmpty(saveData))
			{
				return 0;
			}
			string[] array = saveData.Split(new char[1] { ';' });
			if (array.Length < 2)
			{
				return 0;
			}
			if (!int.TryParse(array[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || result <= 0)
			{
				return 0;
			}
			return result;
		}

		public static List<SavedItem> Offer(string ownerUid, string bagUid, IEnumerable<SavedItem> items)
		{
			return Where(items, (SavedItem it) => it.ItemId > 0 && (IsInPouchOf(it, ownerUid) || IsInBag(it, bagUid)));
		}

		private static List<SavedItem> Where(IEnumerable<SavedItem> items, Func<SavedItem, bool> pred)
		{
			List<SavedItem> list = new List<SavedItem>();
			if (items == null)
			{
				return list;
			}
			foreach (SavedItem item in items)
			{
				if (pred(item))
				{
					list.Add(item);
				}
			}
			return list;
		}

		public static List<int> EnchantmentIds(string subClassesData)
		{
			List<int> list = new List<int>();
			if (string.IsNullOrEmpty(subClassesData))
			{
				return list;
			}
			string text = null;
			string[] array = subClassesData.Split(new char[1] { ';' });
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i].Trim();
				if (text2.Length != 0)
				{
					int num = text2.IndexOf('/');
					string s;
					if (num >= 0)
					{
						text = text2.Substring(0, num);
						s = text2.Substring(num + 1);
					}
					else
					{
						s = text2;
					}
					if (text != null && text.EndsWith("Enchantments", StringComparison.Ordinal) && int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && result > 0 && !list.Contains(result))
					{
						list.Add(result);
					}
				}
			}
			return list;
		}

		public static int NewestFirst(string a, string b)
		{
			return string.CompareOrdinal(b ?? "", a ?? "");
		}
	}
	public static class SaveSnapshotRules
	{
		public enum ItemEdit
		{
			Unchanged,
			Reduced,
			Removed
		}

		public const string SnapshotFormat = "yyyyMMddHHmmss";

		public const string PSaveStampFormat = "yyyy-MM-dd_HH-mm-ss";

		public static string SnapshotName(DateTime now)
		{
			return now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
		}

		public static string PSaveStamp(DateTime now)
		{
			return now.ToString("yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture);
		}

		public static string NextSnapshotName(DateTime now, string newestExisting)
		{
			string text = SnapshotName(now);
			if (string.IsNullOrEmpty(newestExisting) || string.CompareOrdinal(text, newestExisting) > 0)
			{
				return text;
			}
			if (DateTime.TryParseExact(newestExisting, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var result))
			{
				return SnapshotName(result.AddSeconds(1.0));
			}
			return text;
		}

		public static ItemEdit EditItemEntry(string syncDataXml, int qty, out string newXml)
		{
			newXml = syncDataXml;
			if (string.IsNullOrEmpty(syncDataXml) || qty <= 0)
			{
				return ItemEdit.Unchanged;
			}
			try
			{
				XmlDocument xmlDocument = new XmlDocument();
				xmlDocument.LoadXml(syncDataXml);
				XmlElement xmlElement = xmlDocument["Item"];
				if (xmlElement == null)
				{
					return ItemEdit.Unchanged;
				}
				XmlElement xmlElement2 = xmlElement["Quantity"];
				int num = 1;
				if (xmlElement2 != null && int.TryParse(xmlElement2.InnerText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && result > 0)
				{
					num = result;
				}
				if (qty >= num)
				{
					newXml = null;
					return ItemEdit.Removed;
				}
				xmlElement2.InnerText = (num - qty).ToString(CultureInfo.InvariantCulture);
				newXml = xmlDocument.OuterXml;
				return ItemEdit.Reduced;
			}
			catch (XmlException)
			{
				return ItemEdit.Unchanged;
			}
		}
	}
	public enum SpecIssueSeverity
	{
		Warn,
		Error
	}
	public sealed class SpecIssue
	{
		public SpecIssueSeverity Severity;

		public string Message;

		public SpecIssue(SpecIssueSeverity severity, string message)
		{
			Severity = severity;
			Message = message;
		}

		public override string ToString()
		{
			return Severity.ToString() + ": " + Message;
		}
	}
	public static class SpecValidation
	{
		public const int MaxMenuDepth = 3;

		public static List<SpecIssue> Validate(NpcSpec spec)
		{
			List<SpecIssue> list = new List<SpecIssue>();
			if (spec == null)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Error, "spec is null"));
				return list;
			}
			if (string.IsNullOrEmpty(spec.Id))
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Error, "spec has no Id"));
			}
			if (string.IsNullOrEmpty(spec.Name))
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Error, "spec has no Name"));
			}
			bool flag = spec.Trainer != null;
			if (flag && (spec.Trainer.Tree == null || string.IsNullOrEmpty(spec.Trainer.SkillTreeUID)))
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Error, "TrainerSpec present but SkillTreeUID/Tree missing — an unsellable trainer"));
			}
			int num = ((spec.Placements != null) ? spec.Placements.Count : 0);
			if (num == 0)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Warn, "no Placement — spawnable only via verbs."));
			}
			else if (num > 1)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Warn, num + " placements — v1 uses Placements[0] only (rotation/conditions are the deferred §5 axis)."));
			}
			if (spec.Dialogue == null)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Error, "Dialogue is required (an NPC with no DialogueSpec cannot be rigged)"));
				return list;
			}
			if (spec.Combat != null && !spec.Mobile)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Error, "CombatSpec requires Mobile=true — a pinned, AI-less body has nothing to fight with."));
			}
			if (spec.Ai != null && !spec.Mobile)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Warn, "AiSpec is ignored unless Mobile=true (no AI is built for a static NPC)."));
			}
			if (spec.Combat != null && spec.Combat.DamageResists != null && spec.Combat.DamageResists.Length != 6)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Error, "CombatSpec.DamageResists must have exactly 6 entries (Physical, Ethereal, Decay, Electric, Frost, Fire)."));
			}
			if (spec.Merchant != null && !spec.Mobile)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Error, "MerchantSpec requires Mobile=true — the Merchant graft rides the mobile rig, so a static NPC would never get a shop."));
			}
			if (spec.Merchant != null && spec.Merchant.RefreshRateGameHours < 0f)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Error, "MerchantSpec.RefreshRateGameHours must be >= 0."));
			}
			if (spec.OutfitPool != null)
			{
				int num2 = 0;
				foreach (OutfitSpec item in spec.OutfitPool)
				{
					if (item == null || item.IsEmpty)
					{
						num2++;
					}
				}
				if (num2 > 0)
				{
					list.Add(new SpecIssue(SpecIssueSeverity.Warn, num2 + " empty OutfitPool entr" + ((num2 == 1) ? "y" : "ies") + " — rolling one of those leaves the NPC in the spec's explicit ids (or bare)."));
				}
			}
			bool wantsTrain = false;
			bool wantsShop = false;
			int visibleChoices = 0;
			WalkChoices(spec.Dialogue.Choices, 0, new List<Choice>(), list, ref wantsTrain, ref wantsShop, ref visibleChoices);
			if (flag && wantsTrain)
			{
				visibleChoices++;
			}
			bool num3 = spec.Merchant != null;
			if (num3 && wantsShop)
			{
				visibleChoices++;
			}
			if (!num3 && wantsShop)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Error, "a Shop choice with no MerchantSpec — there is no shop to open."));
			}
			if (num3 && !wantsShop)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Warn, "MerchantSpec present but no Shop choice in the dialogue — the shop will be unreachable in conversation."));
			}
			if (visibleChoices == 0)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Warn, "no visible dialogue choices — greeting-only NPC (the conversation ends after the greeting)."));
			}
			if (flag && !wantsTrain)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Warn, "no Train choice in the dialogue — the skill tree will be unreachable in conversation."));
			}
			if (!flag && wantsTrain)
			{
				list.Add(new SpecIssue(SpecIssueSeverity.Warn, "a Train choice with no TrainerSpec — the choice is dropped from the dialogue (plain NPC)."));
			}
			return list;
		}

		private static void WalkChoices(List<Choice> rows, int depth, List<Choice> ancestors, List<SpecIssue> issues, ref bool wantsTrain, ref bool wantsShop, ref int visibleChoices)
		{
			if (rows == null)
			{
				return;
			}
			foreach (Choice row in rows)
			{
				if (row == null)
				{
					continue;
				}
				if (row.Kind == ChoiceKind.Shop)
				{
					if (depth > 0)
					{
						issues.Add(new SpecIssue(SpecIssueSeverity.Warn, "choice '" + row.Id + "': a Shop choice inside a submenu is dropped — Shop belongs at the root."));
					}
					else
					{
						wantsShop = true;
					}
					continue;
				}
				if (row.Kind == ChoiceKind.Train)
				{
					if (depth > 0)
					{
						issues.Add(new SpecIssue(SpecIssueSeverity.Warn, "choice '" + row.Id + "': a Train choice inside a submenu is dropped — Train belongs at the root."));
					}
					else
					{
						wantsTrain = true;
					}
					continue;
				}
				if (row.Kind == ChoiceKind.Action && string.IsNullOrEmpty(row.ActionId))
				{
					issues.Add(new SpecIssue(SpecIssueSeverity.Error, "choice '" + row.Id + "': Kind=Action with no ActionId — there is no callback to run."));
				}
				if (depth == 0)
				{
					visibleChoices++;
				}
				if (row.Kind == ChoiceKind.Menu)
				{
					if (row.Children == null || row.Children.Count == 0)
					{
						issues.Add(new SpecIssue(SpecIssueSeverity.Error, "choice '" + row.Id + "': Kind=Menu with no Children — an empty submenu has nothing to connect."));
					}
					else if (ContainsReference(ancestors, row))
					{
						issues.Add(new SpecIssue(SpecIssueSeverity.Error, "choice '" + row.Id + "': submenu cycle — this menu is reachable from itself."));
					}
					else if (depth + 1 > 3)
					{
						issues.Add(new SpecIssue(SpecIssueSeverity.Error, "choice '" + row.Id + "': submenu nesting exceeds MaxMenuDepth (" + 3 + ")."));
					}
					else
					{
						ancestors.Add(row);
						WalkChoices(row.Children, depth + 1, ancestors, issues, ref wantsTrain, ref wantsShop, ref visibleChoices);
						ancestors.RemoveAt(ancestors.Count - 1);
					}
				}
			}
		}

		private static bool ContainsReference(List<Choice> list, Choice c)
		{
			foreach (Choice item in list)
			{
				if (item == c)
				{
					return true;
				}
			}
			return false;
		}

		public static bool HasErrors(List<SpecIssue> issues)
		{
			if (issues == null)
			{
				return false;
			}
			foreach (SpecIssue issue in issues)
			{
				if (issue.Severity == SpecIssueSeverity.Error)
				{
					return true;
				}
			}
			return false;
		}

		public static string FirstError(List<SpecIssue> issues)
		{
			if (issues == null)
			{
				return null;
			}
			foreach (SpecIssue issue in issues)
			{
				if (issue.Severity == SpecIssueSeverity.Error)
				{
					return issue.Message;
				}
			}
			return null;
		}
	}
	public static class TorsoLook
	{
		public static bool ShouldMute(bool locked, bool hasWeapon, bool rangeSkillAim, bool chargingAttack, bool sprinting)
		{
			if (sprinting)
			{
				return false;
			}
			if (!locked)
			{
				return false;
			}
			if (rangeSkillAim || chargingAttack)
			{
				return false;
			}
			if (hasWeapon)
			{
				return false;
			}
			return true;
		}
	}
	public enum TreeIssueSeverity
	{
		Warn,
		Error
	}
	public sealed class TreeIssue
	{
		public TreeIssueSeverity Severity;

		public string Message;

		public TreeIssue(TreeIssueSeverity severity, string message)
		{
			Severity = severity;
			Message = message;
		}

		public override string ToString()
		{
			return $"{Severity}: {Message}";
		}
	}
	public static class TreeLayout
	{
		public const int MinRow = 1;

		public const int MaxRow = 5;

		public const int MinColumn = 1;

		public const int MaxColumn = 3;

		public const int BreakthroughRow = 3;

		public static List<TreeIssue> Validate(SkillTreeDef tree)
		{
			List<TreeIssue> list = new List<TreeIssue>();
			if (tree == null)
			{
				list.Add(Error("tree is null"));
				return list;
			}
			if (string.IsNullOrEmpty(tree.Name))
			{
				list.Add(Error("tree has no Name"));
			}
			if (tree.Slots == null || tree.Slots.Count == 0)
			{
				list.Add(Error("tree '" + tree.Name + "' has no slots"));
				return list;
			}
			HashSet<(int, int)> hashSet = new HashSet<(int, int)>();
			HashSet<int> hashSet2 = new HashSet<int>();
			foreach (SlotDef s in tree.Slots)
			{
				string text = $"slot ({s.Row},{s.Column}) skill {s.SkillId}";
				if (s.Row < 1 || s.Row > 5)
				{
					list.Add(Error($"{text}: Row must be {1}-{5}"));
				}
				if (s.Column < 1 || s.Column > 3)
				{
					list.Add(Error($"{text}: Column must be {1}-{3}"));
				}
				if (s.SkillId == 0)
				{
					list.Add(Error(text + ": SkillId is 0"));
				}
				if (s.SilverCost < 0)
				{
					list.Add(Error(text + ": negative SilverCost"));
				}
				if (!hashSet.Add((s.Row, s.Column)))
				{
					list.Add(Error($"{text}: duplicate cell — another slot already occupies ({s.Row},{s.Column})"));
				}
				if (s.SkillId != 0 && !hashSet2.Add(s.SkillId))
				{
					list.Add(Error($"{text}: duplicate SkillId {s.SkillId}"));
				}
				if (s.RequiredRow != 0 || s.RequiredColumn != 0)
				{
					if (s.RequiredRow == 0 || s.RequiredColumn == 0)
					{
						list.Add(Error($"{text}: prerequisite is half-set ({s.RequiredRow},{s.RequiredColumn}) — both row and column, or neither"));
					}
					else if (!tree.Slots.Any((SlotDef o) => o != s && o.Row == s.RequiredRow && o.Column == s.RequiredColumn))
					{
						list.Add(Error($"{text}: prerequisite ({s.RequiredRow},{s.RequiredColumn}) names no slot in this tree"));
					}
					if (s.RequiredRow == s.Row && s.RequiredColumn == s.Column)
					{
						list.Add(Error(text + ": prerequisite points at itself"));
					}
				}
				if (s.Breakthrough && s.Row != 3)
				{
					list.Add(Error($"{text}: Breakthrough outside row {3} — vanilla trees keep the breakthrough on row {3}"));
				}
			}
			Dictionary<(int, int), SlotDef> dictionary = new Dictionary<(int, int), SlotDef>();
			foreach (SlotDef slot in tree.Slots)
			{
				if (!dictionary.ContainsKey((slot.Row, slot.Column)))
				{
					dictionary[(slot.Row, slot.Column)] = slot;
				}
			}
			HashSet<(int, int)> hashSet3 = new HashSet<(int, int)>();
			foreach (SlotDef slot2 in tree.Slots)
			{
				HashSet<(int, int)> hashSet4 = new HashSet<(int, int)> { (slot2.Row, slot2.Column) };
				List<string> list2 = new List<string> { $"({slot2.Row},{slot2.Column})" };
				SlotDef slotDef = slot2;
				SlotDef value;
				while (slotDef.RequiredRow != 0 && slotDef.RequiredColumn != 0 && dictionary.TryGetValue((slotDef.RequiredRow, slotDef.RequiredColumn), out value))
				{
					list2.Add($"({value.Row},{value.Column})");
					if (!hashSet4.Add((value.Row, value.Column)))
					{
						if (hashSet3.Add((slot2.Row, slot2.Column)))
						{
							list.Add(Error("tree '" + tree.Name + "': prerequisite CYCLE " + string.Join(" -> ", list2.ToArray()) + " — no slot on this chain can ever be unlocked"));
						}
						break;
					}
					slotDef = value;
				}
			}
			int num = tree.Slots.Count((SlotDef slotDef2) => slotDef2.Breakthrough);
			if (num == 0)
			{
				list.Add(Error("tree '" + tree.Name + "' has no breakthrough slot — vanilla trees have exactly one"));
			}
			else if (num > 1)
			{
				list.Add(Error($"tree '{tree.Name}' has {num} breakthrough slots — vanilla trees have exactly one"));
			}
			return list;
		}

		public static bool HasErrors(IEnumerable<TreeIssue> issues)
		{
			return issues?.Any((TreeIssue i) => i.Severity == TreeIssueSeverity.Error) ?? false;
		}

		private static TreeIssue Error(string msg)
		{
			return new TreeIssue(TreeIssueSeverity.Error, msg);
		}

		private static TreeIssue Warn(string msg)
		{
			return new TreeIssue(TreeIssueSeverity.Warn, msg);
		}
	}
	public struct VisualBounds
	{
		public int Skins;

		public int FemaleSkins;

		public int MaleHeadsWhite;

		public int MaleHeadsBlack;

		public int MaleHeadsAsian;

		public int FemaleHeadsWhite;

		public int FemaleHeadsBlack;

		public int FemaleHeadsAsian;

		public int Hairs;

		public int HairColors;
	}
	public struct VisualPick
	{
		public int Gender;

		public int SkinIndex;

		public int HeadVariationIndex;

		public int HairStyleIndex;

		public int HairColorIndex;

		public override string ToString()
		{
			return $"gender={Gender} skin={SkinIndex} head={HeadVariationIndex} hair={HairStyleIndex} hairColor={HairColorIndex}";
		}
	}
	public static class VisualRoll
	{
		public static int Seed(string id, int sessionSalt)
		{
			uint num = 2166136261u;
			if (id != null)
			{
				foreach (char c in id)
				{
					num ^= c;
					num *= 16777619;
				}
			}
			return (int)num ^ sessionSalt;
		}

		public static double Roll01(int seed, int stream)
		{
			return new Random(seed + stream * 7919).NextDouble();
		}

		public static VisualPick Roll(int seed, VisualBounds b)
		{
			Random random = new Random(seed);
			VisualPick result = new VisualPick
			{
				Gender = random.Next(0, 2)
			};
			int bound = ((result.Gender == 1 && b.FemaleSkins > 0) ? b.FemaleSkins : b.Skins);
			result.SkinIndex = Next(random, bound);
			int bound2 = ((result.SkinIndex == 0) ? ((result.Gender == 0) ? b.MaleHeadsWhite : b.FemaleHeadsWhite) : ((result.SkinIndex != 1) ? ((result.Gender == 0) ? b.MaleHeadsAsian : b.FemaleHeadsAsian) : ((result.Gender == 0) ? b.MaleHeadsBlack : b.FemaleHeadsBlack)));
			result.HeadVariationIndex = Next(random, bound2);
			result.HairStyleIndex = Next(random, b.Hairs);
			result.HairColorIndex = Next(random, b.HairColors);
			return result;
		}

		private static int Next(Random r, int bound)
		{
			if (bound > 0)
			{
				return r.Next(0, bound);
			}
			return 0;
		}
	}
}

plugins/StoryKit.dll

Decompiled 2 weeks ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
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 ForgeKit;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using NodeCanvas.DialogueTrees;
using NodeCanvas.Framework;
using NodeCanvas.Tasks.Actions;
using Photon;
using SideLoader;
using SideLoader.Model;
using StoryKit.Core;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Events;
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("StoryKit")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.12.0")]
[assembly: AssemblyInformationalVersion("0.1.12+de8ede7c1cc4e525b56eb73ca1c0bdd9555d1cc3")]
[assembly: AssemblyProduct("StoryKit")]
[assembly: AssemblyTitle("StoryKit")]
[assembly: AssemblyMetadata("BuildStamp", "de8ede7c 2026-09-05")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

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

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

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
}
namespace StoryKit
{
	public static class ChoiceActions
	{
		private static readonly Dictionary<string, Dictionary<string, Func<Character, string>>> _bySpec = new Dictionary<string, Dictionary<string, Func<Character, string>>>(StringComparer.OrdinalIgnoreCase);

		public static void Register(string specId, string actionId, Func<Character, string> fn)
		{
			if (string.IsNullOrEmpty(specId) || string.IsNullOrEmpty(actionId) || fn == null)
			{
				Plugin.Log.LogWarning((object)$"[STORYKIT] ChoiceActions.Register ignored (specId='{specId}' actionId='{actionId}' fn={fn != null}).");
				return;
			}
			if (!_bySpec.TryGetValue(specId, out var value))
			{
				value = (_bySpec[specId] = new Dictionary<string, Func<Character, string>>(StringComparer.OrdinalIgnoreCase));
			}
			value[actionId] = fn;
		}

		public static void Unregister(string specId)
		{
			if (!string.IsNullOrEmpty(specId) && _bySpec.Remove(specId))
			{
				Plugin.Log.LogMessage((object)("[STORYKIT] ChoiceActions: dropped the callback table for '" + specId + "'."));
			}
		}

		public static bool TryGet(string specId, string actionId, out Func<Character, string> fn)
		{
			fn = null;
			if (string.IsNullOrEmpty(specId) || string.IsNullOrEmpty(actionId))
			{
				return false;
			}
			if (_bySpec.TryGetValue(specId, out var value))
			{
				return value.TryGetValue(actionId, out fn);
			}
			return false;
		}

		public static bool Has(string specId, string actionId)
		{
			Func<Character, string> fn;
			return TryGet(specId, actionId, out fn);
		}
	}
	internal static class DialogueBuilder
	{
		private sealed class PendingEdge
		{
			public Node Source;

			public Node Target;

			public int SourceIndex;

			public string Label;
		}

		private sealed class MenuCtx
		{
			public DialogueTree Graph;

			public string SpecId;

			public string ActorName;

			public StatementNodeExt Greet;

			public ActionNode TrainNode;

			public ActionNode ShopNode;

			public bool WantsTrain;

			public List<Node> Emitted = new List<Node>();

			public List<PendingEdge> Pending = new List<PendingEdge>();

			public StringBuilder Outline = new StringBuilder();

			public int Refused;
		}

		private sealed class OrphanStash
		{
			public DialogueTree Graph;

			public readonly List<Node> Nodes = new List<Node>();
		}

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

		internal static void Build(NpcSpec spec, Character trainerChar)
		{
			try
			{
				BuildInner(spec, trainerChar);
			}
			catch (Exception ex)
			{
				if (spec.Trainer == null)
				{
					Plugin.Log.LogWarning((object)("[STORYKIT] dialogue surgery FAILED for '" + spec.Id + "' (plain dialogue NPC) — falling back to a GREETING-ONLY graph; SL's default train node is removed because this spec " + $"has no skill tree to open: {ex}"));
					MakeGreetingOnly(spec, trainerChar);
				}
				else
				{
					Plugin.Log.LogWarning((object)$"[STORYKIT] dialogue surgery FAILED for '{spec.Id}' — SL's default greeting→train graph stays in effect: {ex}");
				}
				throw;
			}
		}

		private static void MakeGreetingOnly(NpcSpec spec, Character trainerChar)
		{
			//IL_0094: Unknown result type (might be due to invalid IL or missing references)
			//IL_009e: Expected O, but got Unknown
			try
			{
				DialogueTreeController componentInChildren = ((Component)trainerChar).GetComponentInChildren<DialogueTreeController>(true);
				DialogueTree val = (DialogueTree)(((Object)(object)componentInChildren != (Object)null) ? /*isinst with value type is only supported in some contexts*/: null);
				if ((Object)(object)val == (Object)null)
				{
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "': no DialogueTree to make safe — the NPC may open an empty trainer panel."));
					return;
				}
				DialogueActor componentInChildren2 = ((Component)trainerChar).GetComponentInChildren<DialogueActor>(true);
				string text = ((spec.Dialogue != null && spec.Dialogue.Greetings.Count > 0) ? spec.Dialogue.Greetings[0] : "...");
				StatementNodeExt val2 = ((Graph)val).AddNode<StatementNodeExt>();
				val2.statement = new Statement(text);
				if ((Object)(object)componentInChildren2 != (Object)null)
				{
					val2.SetActorName(componentInChildren2.name);
				}
				PurgeOrphans(spec.Id, val, new List<Node> { (Node)(object)val2 });
				((Graph)val).allNodes.Clear();
				((Graph)val).allNodes.Add((Node)(object)val2);
				((Graph)val).primeNode = (Node)(object)val2;
				Plugin.Log.LogMessage((object)("[STORYKIT] '" + spec.Id + "': greeting-only fallback graph installed (1 node, no train path)."));
			}
			catch (Exception arg)
			{
				Plugin.Log.LogWarning((object)$"[STORYKIT] '{spec.Id}': greeting-only fallback ALSO failed: {arg}");
			}
		}

		private static void BuildInner(NpcSpec spec, Character trainerChar)
		{
			//IL_020b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0312: Unknown result type (might be due to invalid IL or missing references)
			//IL_0345: Unknown result type (might be due to invalid IL or missing references)
			//IL_034a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0429: Unknown result type (might be due to invalid IL or missing references)
			//IL_0433: Expected O, but got Unknown
			//IL_0658: Unknown result type (might be due to invalid IL or missing references)
			//IL_0662: Expected O, but got Unknown
			//IL_06b7: Unknown result type (might be due to invalid IL or missing references)
			//IL_0946: Unknown result type (might be due to invalid IL or missing references)
			if (!spec.Mobile && spec.Combat == null)
			{
				Object.DestroyImmediate((Object)(object)((Component)trainerChar).GetComponent<CharacterStats>());
			}
			Object.DestroyImmediate((Object)(object)((Component)trainerChar).GetComponent<StartingEquipment>());
			DialogueActor componentInChildren = ((Component)trainerChar).GetComponentInChildren<DialogueActor>(true);
			Trainer componentInChildren2 = ((Component)trainerChar).GetComponentInChildren<Trainer>(true);
			DialogueTreeController componentInChildren3 = ((Component)trainerChar).GetComponentInChildren<DialogueTreeController>(true);
			DialogueTree val = (DialogueTree)(((Object)(object)componentInChildren3 != (Object)null) ? /*isinst with value type is only supported in some contexts*/: null);
			int num = ((Component)trainerChar).gameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true).Length;
			DialogueActorLocalize componentInChildren4 = ((Component)trainerChar).GetComponentInChildren<DialogueActorLocalize>(true);
			string text = (((Object)(object)componentInChildren4 != (Object)null) ? componentInChildren4.LocKey : null);
			Plugin.Log.LogMessage((object)($"[STORYKIT] rig census '{spec.Id}': master={!PhotonNetwork.isNonMasterClientInRoom} " + $"actor={(Object)(object)componentInChildren != (Object)null} trainer={(Object)(object)componentInChildren2 != (Object)null} controller={(Object)(object)componentInChildren3 != (Object)null} " + "actorKey=" + (string.IsNullOrEmpty(text) ? "none" : ("'" + text + "'")) + " " + $"visuals={num} renderers" + (((Object)(object)val != (Object)null) ? string.Format(" graphNodes={0} node1={1}", ((Graph)val).allNodes.Count, (((Graph)val).allNodes.Count <= 1) ? "none" : ((object)((Graph)val).allNodes[1])?.GetType().Name) : " graph=none")));
			if (((Object)(object)componentInChildren == (Object)null || (Object)(object)componentInChildren3 == (Object)null || (Object)(object)componentInChildren2 == (Object)null) && PhotonNetwork.isNonMasterClientInRoom)
			{
				GameObject val2 = Resources.Load<GameObject>("editor/templates/TrainerTemplate");
				if ((Object)(object)val2 != (Object)null)
				{
					GameObject val3 = Object.Instantiate<GameObject>(val2, ((Component)trainerChar).transform, false);
					val3.transform.localPosition = Vector3.zero;
					((Object)val3).name = "TrainerRig_StoryKit";
					componentInChildren = ((Component)trainerChar).GetComponentInChildren<DialogueActor>(true);
					componentInChildren2 = ((Component)trainerChar).GetComponentInChildren<Trainer>(true);
					componentInChildren3 = ((Component)trainerChar).GetComponentInChildren<DialogueTreeController>(true);
					Plugin.Log.LogMessage((object)("[STORYKIT] '" + spec.Id + "': guest-side rig instantiated from " + $"'editor/templates/TrainerTemplate' (actor={(Object)(object)componentInChildren != (Object)null} trainer={(Object)(object)componentInChildren2 != (Object)null} " + $"controller={(Object)(object)componentInChildren3 != (Object)null})."));
				}
				else
				{
					Plugin.Log.LogWarning((object)"[STORYKIT] guest rig fallback: Resources.Load('editor/templates/TrainerTemplate') returned null — the template path moved?");
				}
			}
			if ((Object)(object)componentInChildren == (Object)null || (Object)(object)componentInChildren3 == (Object)null)
			{
				throw new InvalidOperationException($"trainer rig incomplete: DialogueActor={(Object)(object)componentInChildren != (Object)null} DialogueTreeController={(Object)(object)componentInChildren3 != (Object)null}");
			}
			if ((Object)(object)componentInChildren2 != (Object)null && spec.Trainer != null && !string.Equals(UID.op_Implicit(componentInChildren2.m_skillTreeUID), spec.Trainer.SkillTreeUID, StringComparison.Ordinal))
			{
				componentInChildren2.m_skillTreeUID = UID.op_Implicit(spec.Trainer.SkillTreeUID);
				Plugin.Log.LogMessage((object)("[STORYKIT] '" + spec.Id + "': stamped skill-tree UID '" + spec.Trainer.SkillTreeUID + "' onto the Trainer."));
			}
			componentInChildren.SetName(spec.Name);
			AttachActorLocKey(spec, trainerChar, componentInChildren);
			Graph graph = ((GraphOwner)componentInChildren3).graph;
			DialogueTree val4 = (DialogueTree)(object)((graph is DialogueTree) ? graph : null);
			if ((Object)(object)val4 == (Object)null)
			{
				throw new InvalidOperationException("controller.graph is not a DialogueTree");
			}
			List<ActorParameter> list = val4._actorParameters;
			if (list == null)
			{
				list = (val4._actorParameters = new List<ActorParameter>());
			}
			if (list.Count == 0)
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "': the dialogue graph shipped no ActorParameter (guest replica or an untouched template rig?) — adding one so the actor can bind."));
				list.Add(new ActorParameter());
			}
			list[0].actor = (IDialogueActor)(object)componentInChildren;
			list[0].name = componentInChildren.name;
			ActionNode val5 = (ActionNode)((((Graph)val4).allNodes.Count > 1) ? /*isinst with value type is only supported in some contexts*/: null);
			TrainDialogueAction val6 = (TrainDialogueAction)((val5 != null) ? /*isinst with value type is only supported in some contexts*/: null);
			bool flag = spec.Trainer != null && spec.Dialogue.Choices.Exists((Choice c) => (int)c.Kind == 0);
			if (!flag)
			{
				val5 = null;
				val6 = null;
			}
			if (flag && (val5 == null || val6 == null))
			{
				if ((Object)(object)componentInChildren2 == (Object)null)
				{
					throw new InvalidOperationException("no Trainer component to open the panel with — rig incomplete");
				}
				val5 = ((Graph)val4).AddNode<ActionNode>();
				val5.action = (ActionTask)(object)new OpenTrainerTask
				{
					Trainer = componentInChildren2
				};
				val6 = null;
				Plugin.Log.LogMessage((object)("[STORYKIT] '" + spec.Id + "': SL's TrainDialogueAction not at allNodes[1] — built a from-scratch train node (guest rig fix, docs/guest-pets-plan.md M5)."));
			}
			if (val6 != null && (Object)(object)componentInChildren2 != (Object)null)
			{
				val6.Trainer = new BBParameter<Trainer>(componentInChildren2);
			}
			Merchant val7 = ((spec.Merchant != null) ? ((Component)trainerChar).GetComponentInChildren<Merchant>(true) : null);
			bool flag2 = spec.Merchant != null && spec.Dialogue.Choices.Exists((Choice c) => c != null && (int)c.Kind == 3);
			ActionNode val8 = null;
			if (flag2)
			{
				if ((Object)(object)val7 == (Object)null)
				{
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "': Shop choice but no Merchant component on the body (graft failed?) — the row is dropped."));
				}
				else
				{
					val8 = ((Graph)val4).AddNode<ActionNode>();
					val8.action = (ActionTask)(object)new OpenShopTask(val7, spec.Id);
				}
			}
			string text2 = ((spec.Dialogue.Greetings.Count > 0) ? spec.Dialogue.Greetings[0] : "...");
			StatementNodeExt val9 = ((Graph)val4).AddNode<StatementNodeExt>();
			val9.statement = new Statement(text2);
			val9.SetActorName(componentInChildren.name);
			MenuPlan val10 = MenuPlanner.Plan((IList<Choice>)spec.Dialogue.Choices, flag, val5 != null, (string)null, flag2, val8 != null, (Func<string, bool>)((string id) => ChoiceActions.Has(spec.Id, id)));
			WarnDrops(spec.Id, val10);
			int num2 = val10.VisibleRowCount(val10.Root);
			if (num2 == 0)
			{
				PurgeOrphans(spec.Id, val4, new List<Node> { (Node)(object)val9 });
				((Graph)val4).allNodes.Clear();
				((Graph)val4).allNodes.Add((Node)(object)val9);
				((Graph)val4).primeNode = (Node)(object)val9;
				((Component)trainerChar).gameObject.SetActive(true);
				Plugin.Log.LogMessage((object)("[STORYKIT] dialogue built for '" + spec.Id + "': prime='" + Trim(text2) + "', no visible choices — greeting-only graph (1 node)."));
				return;
			}
			MenuCtx menuCtx = new MenuCtx
			{
				Graph = val4,
				SpecId = spec.Id,
				ActorName = componentInChildren.name,
				Greet = val9,
				TrainNode = val5,
				WantsTrain = flag,
				ShopNode = val8
			};
			MultipleChoiceNodeExt target = EmitMenu(menuCtx, val10);
			List<Node> list2 = new List<Node> { (Node)(object)val9 };
			if (val5 != null)
			{
				list2.Add((Node)(object)val5);
			}
			if (val8 != null)
			{
				list2.Add((Node)(object)val8);
			}
			foreach (Node item in menuCtx.Emitted)
			{
				list2.Add(item);
			}
			PurgeOrphans(spec.Id, val4, list2);
			((Graph)val4).allNodes.Clear();
			foreach (Node item2 in list2)
			{
				((Graph)val4).allNodes.Add(item2);
			}
			((Graph)val4).primeNode = (Node)(object)val9;
			Connect(menuCtx, (Node)(object)val9, (Node)(object)target, -1, "<greeting>");
			foreach (PendingEdge item3 in menuCtx.Pending)
			{
				Connect(menuCtx, item3.Source, item3.Target, item3.SourceIndex, item3.Label);
			}
			((Component)trainerChar).gameObject.SetActive(true);
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append($"[STORYKIT] dialogue built for '{spec.Id}': prime='{Trim(text2)}', {num2} root choices");
			stringBuilder.Append((object?)menuCtx.Outline);
			string text3 = (((Object)(object)componentInChildren2 != (Object)null) ? $"{componentInChildren2.m_skillTreeUID}" : "");
			stringBuilder.Append($"\n  totals: menus={Count(val10, (MenuNodeKind)2)} leaves={Count(val10, (MenuNodeKind)3)} " + $"maxDepth={MaxDepth(val10)} edges={val10.Edges.Count + 1} " + $"refused={val10.Drops.Count + menuCtx.Refused}");
			stringBuilder.Append(string.Format("\n  nodes={0}, trainer={1}", ((Graph)val4).allNodes.Count, (spec.Trainer != null) ? ("UID='" + text3 + "'") : "none (plain dialogue NPC)") + ", shop=" + ((val8 != null) ? "bound" : ((spec.Merchant != null) ? "WANTED BUT UNBOUND" : "none")));
			Plugin.Log.LogMessage((object)stringBuilder.ToString());
			NpcLookFollow npcLookFollow = ((Component)trainerChar).GetComponent<NpcLookFollow>();
			if ((Object)(object)npcLookFollow == (Object)null)
			{
				npcLookFollow = ((Component)trainerChar).gameObject.AddComponent<NpcLookFollow>();
			}
			((Behaviour)npcLookFollow).enabled = spec.LookFollowEnabled;
		}

		private static void AttachActorLocKey(NpcSpec spec, Character trainerChar, DialogueActor actor)
		{
			string text = DialogueLockRules.KeyFor(spec.Id);
			if (!DialogueLockRules.CanLock(text))
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "': no usable actor loc key — this NPC stays outside vanilla's MP dialogue lock (two players can open it at once)."));
				return;
			}
			try
			{
				LocalizationManager instance = LocalizationManager.Instance;
				Dictionary<string, string> dictionary = (((Object)(object)instance != (Object)null) ? instance.m_generalLocalization : null);
				if (dictionary != null && DialogueLockRules.NeedsLocEntry(text, spec.Name, dictionary.ContainsKey(text)))
				{
					dictionary[text] = spec.Name;
				}
				DialogueActorLocalize val = ((Component)actor).GetComponent<DialogueActorLocalize>();
				if ((Object)(object)val == (Object)null)
				{
					val = ((Component)actor).gameObject.AddComponent<DialogueActorLocalize>();
				}
				val.LocKey = text;
				NPCInteraction componentInChildren = ((Component)trainerChar).GetComponentInChildren<NPCInteraction>(true);
				if ((Object)(object)componentInChildren != (Object)null)
				{
					componentInChildren.m_dialogueActorLocalize = val;
				}
				Plugin.Log.LogMessage((object)("[STORYKIT] '" + spec.Id + "': actor loc key '" + text + "' stamped " + $"(interaction={(Object)(object)componentInChildren != (Object)null}) — the NPC now enters vanilla's MP dialogue lock."));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "': stamping the actor loc key threw — the NPC stays outside the MP dialogue lock: " + ex.Message));
			}
		}

		private static string Trim(string s)
		{
			if (s == null || s.Length <= 30)
			{
				return s;
			}
			return s.Substring(0, 30) + "…";
		}

		private static MultipleChoiceNodeExt EmitMenu(MenuCtx ctx, MenuPlan plan)
		{
			//IL_002b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Invalid comparison between Unknown and I4
			//IL_00dc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e2: Invalid comparison between Unknown and I4
			//IL_0130: Unknown result type (might be due to invalid IL or missing references)
			//IL_013a: Expected O, but got Unknown
			//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)
			//IL_0066: Unknown result type (might be due to invalid IL or missing references)
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0078: Expected O, but got Unknown
			//IL_007d: Expected O, but got Unknown
			//IL_019f: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a5: Invalid comparison between Unknown and I4
			//IL_01ae: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b4: Invalid comparison between Unknown and I4
			//IL_023b: Unknown result type (might be due to invalid IL or missing references)
			//IL_024f: Unknown result type (might be due to invalid IL or missing references)
			//IL_02b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_02bb: Expected O, but got Unknown
			Node[] array = (Node[])(object)new Node[plan.Nodes.Count];
			foreach (MenuNode node in plan.Nodes)
			{
				if ((int)node.Kind == 2)
				{
					MultipleChoiceNodeExt val = ((Graph)ctx.Graph).AddNode<MultipleChoiceNodeExt>();
					foreach (string label in node.Labels)
					{
						val.availableChoices.Add(new Choice
						{
							statement = new Statement
							{
								text = label
							}
						});
					}
					array[node.Id] = (Node)(object)val;
					if (node.BackIndex == 0)
					{
						Plugin.Log.LogWarning((object)($"[STORYKIT] '{ctx.SpecId}': submenu at depth {node.Depth} has NO " + "visible rows — it will render as a Back entry and nothing else."));
					}
				}
				else if ((int)node.Kind == 5)
				{
					ActionNode val2 = ((Graph)ctx.Graph).AddNode<ActionNode>();
					val2.action = (ActionTask)(object)new RunActionTask(ctx.SpecId, node.ActionId, node.ChoiceId);
					array[node.Id] = (Node)(object)val2;
				}
				else
				{
					StatementNodeExt val3 = ((Graph)ctx.Graph).AddNode<StatementNodeExt>();
					val3.statement = new Statement(node.ReplyText);
					val3.SetActorName(ctx.ActorName);
					array[node.Id] = (Node)(object)val3;
				}
				ctx.Emitted.Add(array[node.Id]);
			}
			foreach (MenuEdge edge in plan.Edges)
			{
				if ((int)edge.Source.Kind == 5 && (int)edge.Target.Kind == 3)
				{
					Node obj = array[edge.Source.Id];
					Node obj2 = ((obj is ActionNode) ? obj : null);
					if (((obj2 != null) ? ((ActionNode)obj2).action : null) is RunActionTask runActionTask)
					{
						ref StatementNodeExt reply = ref runActionTask.Reply;
						Node obj3 = array[edge.Target.Id];
						reply = (StatementNodeExt)(object)((obj3 is StatementNodeExt) ? obj3 : null);
					}
				}
			}
			foreach (MenuEdge edge2 in plan.Edges)
			{
				ctx.Pending.Add(new PendingEdge
				{
					Source = Resolve(ctx, array, edge2.Source),
					Target = Resolve(ctx, array, edge2.Target),
					SourceIndex = edge2.SourceIndex,
					Label = edge2.Label
				});
			}
			ctx.Outline.Append(plan.Outline);
			return (MultipleChoiceNodeExt)array[plan.Root.Id];
		}

		internal static void FlushStashedOrphans()
		{
			if (_orphanStashes.Count == 0)
			{
				return;
			}
			List<string> list = null;
			foreach (KeyValuePair<string, OrphanStash> orphanStash in _orphanStashes)
			{
				if (!NpcRegistry.IsInDialogue(orphanStash.Key))
				{
					Disconnect(orphanStash.Key, orphanStash.Value.Graph, orphanStash.Value.Nodes, "the conversation ended");
					(list ?? (list = new List<string>())).Add(orphanStash.Key);
				}
			}
			if (list == null)
			{
				return;
			}
			foreach (string item in list)
			{
				_orphanStashes.Remove(item);
			}
		}

		private static void PurgeOrphans(string specId, DialogueTree graph, IList<Node> keep)
		{
			try
			{
				List<Node> list = new List<Node>();
				foreach (Node allNode in ((Graph)graph).allNodes)
				{
					if (allNode == null)
					{
						continue;
					}
					bool flag = false;
					for (int i = 0; i < keep.Count; i++)
					{
						if (keep[i] == allNode)
						{
							flag = true;
							break;
						}
					}
					if (!flag)
					{
						list.Add(allNode);
					}
				}
				if (list.Count == 0)
				{
					return;
				}
				if (NpcRegistry.IsInDialogue(specId))
				{
					if (!_orphanStashes.TryGetValue(specId, out var value))
					{
						value = (_orphanStashes[specId] = new OrphanStash());
					}
					value.Graph = graph;
					value.Nodes.AddRange(list);
					Plugin.Log.LogMessage((object)("[STORYKIT] '" + specId + "': dialogue graph rebuilt while a conversation/shop is live — " + $"the running tree keeps its own nodes; {list.Count} orphaned node(s) stashed " + $"({value.Nodes.Count} pending) and released when the conversation ends."));
				}
				else
				{
					Disconnect(specId, graph, list, "rebuild");
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] '" + specId + "': orphan purge threw (the graph is still rebuilt): " + ex.Message));
			}
		}

		private static void Disconnect(string specId, DialogueTree graph, List<Node> nodes, string why)
		{
			if ((Object)(object)graph == (Object)null || nodes == null || nodes.Count == 0)
			{
				return;
			}
			int num = 0;
			foreach (Node node in nodes)
			{
				if (node == null)
				{
					continue;
				}
				try
				{
					Connection[] array = node.inConnections.ToArray();
					foreach (Connection val in array)
					{
						((Graph)graph).RemoveConnection(val, false);
						num++;
					}
					Connection[] array2 = node.outConnections.ToArray();
					foreach (Connection val2 in array2)
					{
						((Graph)graph).RemoveConnection(val2, false);
						num++;
					}
					node.OnDestroy();
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + specId + "': releasing an orphaned dialogue node threw: " + ex.Message));
				}
			}
			nodes.Clear();
			Plugin.Log.LogMessage((object)$"[STORYKIT] '{specId}': released {num} stale dialogue connection(s) from orphaned nodes ({why}).");
		}

		private static void WarnDrops(string specId, MenuPlan plan)
		{
			//IL_001a: Unknown result type (might be due to invalid IL or missing references)
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Expected I4, but got Unknown
			foreach (MenuDrop drop in plan.Drops)
			{
				MenuDropReason reason = drop.Reason;
				switch ((int)reason)
				{
				case 0:
					Plugin.Log.LogWarning((object)($"[STORYKIT] '{specId}': choice row #{drop.Index} at depth {drop.Depth} " + "is null — dropped. (A spec-authoring slip; the rest of the dialogue is built normally.)"));
					break;
				case 2:
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + specId + "': choice '" + drop.ChoiceId + "' is a Train row inside a " + $"submenu (depth {drop.Depth}) — dropped (Train belongs at the root menu)."));
					break;
				case 5:
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + specId + "': choice '" + drop.ChoiceId + "' is a Shop row inside a " + $"submenu (depth {drop.Depth}) — dropped (Shop belongs at the root menu)."));
					break;
				case 4:
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + specId + "': choice '" + drop.ChoiceId + "' is a Shop row with no shop to open (no MerchantSpec, or the Merchant graft failed) — dropped."));
					break;
				case 6:
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + specId + "': choice '" + drop.ChoiceId + "' is an Action row with no registered callback (ChoiceActions.Register was never called for this spec/action id, or ran after the rig) — dropped."));
					break;
				case 3:
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + specId + "': choice '" + drop.ChoiceId + "' is a Train row but SideLoader's train ActionNode is missing from the graph — dropped (a shown row with no out-connection silently renames every later row)."));
					break;
				default:
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + specId + "': choice '" + drop.ChoiceId + "' is a Train row on a trainer-less NPC — dropped (there is no school to open)."));
					break;
				}
			}
		}

		private static int Count(MenuPlan plan, MenuNodeKind kind)
		{
			//IL_0019: 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)
			int num = 0;
			foreach (MenuNode node in plan.Nodes)
			{
				if (node.Kind == kind)
				{
					num++;
				}
			}
			return num;
		}

		private static int MaxDepth(MenuPlan plan)
		{
			int num = 0;
			foreach (MenuNode node in plan.Nodes)
			{
				if (node.Depth > num)
				{
					num = node.Depth;
				}
			}
			return num;
		}

		private static Node Resolve(MenuCtx ctx, Node[] made, MenuNodeRef r)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//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_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: Expected I4, but got Unknown
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			MenuNodeKind kind = r.Kind;
			return (Node)((int)kind switch
			{
				0 => ctx.Greet, 
				1 => ctx.TrainNode, 
				4 => ctx.ShopNode, 
				_ => made[r.Id], 
			});
		}

		private static void Connect(MenuCtx ctx, Node source, Node target, int sourceIndex, string label)
		{
			if (((Graph)ctx.Graph).ConnectNodes(source, target, sourceIndex, -1) == null)
			{
				ctx.Refused++;
				Plugin.Log.LogError((object)("[STORYKIT] '" + ctx.SpecId + "': dialogue edge REFUSED for '" + label + "' " + $"(slot {sourceIndex}) — that choice will be a dead row."));
			}
		}
	}
	internal class OpenTrainerTask : ActionTask
	{
		public Trainer Trainer;

		public override string info => "Open training menu (StoryKit)";

		public override void OnExecute()
		{
			Character val = Lifecycle.FirstLocalCharacterOrNull();
			if ((Object)(object)Trainer != (Object)null && (Object)(object)val != (Object)null)
			{
				Trainer.StartTraining(val);
			}
			else
			{
				Plugin.Log.LogWarning((object)$"[STORYKIT] train choice clicked but Trainer={(Object)(object)Trainer != (Object)null} player={(Object)(object)val != (Object)null} — panel not opened.");
			}
			((ActionTask)this).EndAction();
		}
	}
	internal class RunActionTask : ActionTask
	{
		private readonly string _specId;

		private readonly string _actionId;

		private readonly string _choiceId;

		private string _specText;

		public StatementNodeExt Reply;

		public override string info => "Run choice action (StoryKit)";

		public RunActionTask(string specId, string actionId, string choiceId)
		{
			_specId = specId;
			_actionId = actionId;
			_choiceId = choiceId;
		}

		public override void OnExecute()
		{
			//IL_00d7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e1: Expected O, but got Unknown
			Character arg = Lifecycle.FirstLocalCharacterOrNull();
			if (!ChoiceActions.TryGet(_specId, _actionId, out var fn))
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] '" + _specId + "': choice '" + _choiceId + "' fired action '" + _actionId + "' but no handler is registered — the spec's reply text stands."));
				((ActionTask)this).EndAction();
				return;
			}
			try
			{
				string text = fn(arg);
				if (Reply == null)
				{
					((ActionTask)this).EndAction();
					return;
				}
				if (_specText == null)
				{
					_specText = ((Reply.statement != null) ? Reply.statement.text : "...");
				}
				Reply.statement = new Statement(string.IsNullOrEmpty(text) ? _specText : text);
			}
			catch (Exception arg2)
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] '" + _specId + "': choice action '" + _actionId + "' THREW " + $"(the conversation continues with the spec's reply text): {arg2}"));
			}
			((ActionTask)this).EndAction();
		}
	}
	internal class OpenShopTask : ShopDialogueAction
	{
		private readonly Merchant _merchant;

		private readonly string _specId;

		public override string info => "Open shop (StoryKit)";

		public OpenShopTask(Merchant merchant, string specId)
		{
			_merchant = merchant;
			_specId = specId;
			base.Merchant = new BBParameter<Merchant>(merchant);
			base.PlayerCharacter = new BBParameter<Character>();
		}

		public override void OnExecute()
		{
			Character val = Lifecycle.FirstLocalCharacterOrNull();
			if ((Object)(object)_merchant == (Object)null || (Object)(object)val == (Object)null)
			{
				Plugin.Log.LogWarning((object)$"[STORYKIT] '{_specId}': shop choice clicked but Merchant={(Object)(object)_merchant != (Object)null} player={(Object)(object)val != (Object)null} — shop not opened.");
				((ActionTask)this).EndAction(false);
				return;
			}
			if ((Object)(object)_merchant.MerchantPouch == (Object)null)
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] '" + _specId + "': shop choice clicked but the Merchant has no pouch yet (Initialize not run?) — shop not opened."));
				((ActionTask)this).EndAction(false);
				return;
			}
			base.Merchant = new BBParameter<Merchant>(_merchant);
			base.PlayerCharacter = new BBParameter<Character>(val);
			if (!PhotonNetwork.isNonMasterClientInRoom)
			{
				try
				{
					NpcSpec val2 = NpcRegistry.Find(_specId);
					if (val2 != null)
					{
						MerchantWiring.RefreshAndMaybeFillFallback(val2, _merchant, out var _);
					}
					else
					{
						_merchant.RefreshInventory();
					}
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + _specId + "': RefreshInventory threw: " + ex.Message));
				}
			}
			Plugin.Log.LogMessage((object)$"[STORYKIT] '{_specId}': opening shop for '{val.Name}' ({_merchant.MerchantPouch.ItemCount} stack(s) in stock).");
			((ShopDialogueAction)this).OnExecute();
		}
	}
	[HarmonyPatch(typeof(DialoguePanel), "OnSelectDialogueOption")]
	internal static class DialoguePanel_RememberSelection_OnSelect
	{
		private static void Prefix(DialoguePanel __instance, int _value)
		{
			if ((Object)(object)__instance == (Object)null || __instance.m_currentOptions == null)
			{
				return;
			}
			foreach (KeyValuePair<IStatement, int> option in __instance.m_currentOptions.options)
			{
				if (option.Value == _value)
				{
					DialogueSelectionMemory.Remember(option.Key);
					break;
				}
			}
		}
	}
	[HarmonyPatch(typeof(DialoguePanel), "RefreshMultipleChoices")]
	internal static class DialoguePanel_RememberSelection_Refresh
	{
		private static void Postfix(DialoguePanel __instance)
		{
			if ((Object)(object)__instance == (Object)null || __instance.m_currentOptions == null)
			{
				return;
			}
			IStatement last = DialogueSelectionMemory.Last;
			if (last == null)
			{
				return;
			}
			int num = 0;
			bool flag = false;
			foreach (KeyValuePair<IStatement, int> option in __instance.m_currentOptions.options)
			{
				if (option.Key == last)
				{
					flag = true;
					break;
				}
				num++;
			}
			if (flag && num < __instance.m_dialogueOptions.Count)
			{
				__instance.m_dialogueOptions[num].Select();
			}
			else
			{
				DialogueSelectionMemory.Forget();
			}
		}
	}
	internal static class DialogueSelectionMemory
	{
		internal static IStatement Last { get; private set; }

		internal static void Remember(IStatement statement)
		{
			Last = statement;
		}

		internal static void Forget()
		{
			Last = null;
		}
	}
	internal static class MerchantWiring
	{
		internal const string ChildName = "MerchantSettings_StoryKit";

		private static readonly Dictionary<string, Transform> _tableCache = new Dictionary<string, Transform>();

		private static readonly Dictionary<string, string> _tableCacheVia = new Dictionary<string, string>();

		internal static Merchant Attach(NpcSpec spec, Character c)
		{
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Expected O, but got Unknown
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			if (spec.Merchant == null || (Object)(object)c == (Object)null)
			{
				return null;
			}
			Merchant componentInChildren = ((Component)c).GetComponentInChildren<Merchant>(true);
			if ((Object)(object)componentInChildren != (Object)null)
			{
				return componentInChildren;
			}
			try
			{
				MerchantSpec merchant = spec.Merchant;
				DialogueActor componentInChildren2 = ((Component)c).GetComponentInChildren<DialogueActor>(true);
				Transform val = (((Object)(object)componentInChildren2 != (Object)null) ? ((Component)componentInChildren2).transform : ((Component)c).transform);
				GameObject val2 = new GameObject("MerchantSettings_StoryKit");
				val2.SetActive(false);
				val2.transform.SetParent(val, false);
				val2.transform.localPosition = Vector3.zero;
				Merchant val3 = val2.AddComponent<Merchant>();
				val3.SetHolderUID(new UID(spec.Id + ".merchant"));
				val3.NonSavableInventory = merchant.NonSavable;
				val3.m_isBuyer = merchant.Buyer;
				val3.m_isSeller = merchant.Seller;
				val3.m_inventoryRefreshRate = merchant.RefreshRateGameHours;
				string via;
				Transform val4 = (val3.m_merchantInventoryTablePrefab = FindStockTable(merchant.StockTableNameContains, out via, includeAssets: false));
				val2.SetActive(true);
				Plugin.Log.LogMessage((object)("[STORYKIT] '" + spec.Id + "': Merchant grafted (uid '" + spec.Id + ".merchant', " + $"buyer={merchant.Buyer} seller={merchant.Seller} nonSavable={merchant.NonSavable} refresh={merchant.RefreshRateGameHours}h, " + "stock=" + (((Object)(object)val4 != (Object)null) ? ("'" + ((Object)val4).name + "' via " + via) : "none") + (((Object)(object)val4 == (Object)null && merchant.FallbackItemNames != null && merchant.FallbackItemNames.Count > 0) ? $", fallback items={merchant.FallbackItemNames.Count}" : "") + ")."));
				if ((Object)(object)Plugin.Instance != (Object)null)
				{
					((MonoBehaviour)Plugin.Instance).StartCoroutine(AfterInit(spec, val3));
				}
				else
				{
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "': no plugin host — the shop's first stock roll is deferred to the first open."));
				}
				return val3;
			}
			catch (Exception arg)
			{
				Plugin.Log.LogWarning((object)$"[STORYKIT] '{spec.Id}': Merchant graft FAILED — the NPC talks but cannot trade: {arg}");
				return null;
			}
		}

		private static IEnumerator AfterInit(NpcSpec spec, Merchant m)
		{
			float t0 = Time.unscaledTime;
			while (Time.unscaledTime - t0 < 5f)
			{
				if ((Object)(object)m == (Object)null || !Object.op_Implicit((Object)(object)m))
				{
					yield break;
				}
				if ((Object)(object)m.MerchantPouch != (Object)null)
				{
					break;
				}
				yield return null;
			}
			if ((Object)(object)m == (Object)null || !Object.op_Implicit((Object)(object)m))
			{
				yield break;
			}
			if ((Object)(object)m.MerchantPouch == (Object)null)
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "': Merchant pouch not minted 5s after graft (ResourcesPrefabManager not loaded?) — the shop may be empty."));
			}
			else
			{
				if (PhotonNetwork.isNonMasterClientInRoom)
				{
					yield break;
				}
				if (spec.Merchant != null && (Object)(object)m.m_merchantInventoryTablePrefab == (Object)null && (Object)(object)m.DropableInventory == (Object)null)
				{
					string via;
					Transform val = FindStockTable(spec.Merchant.StockTableNameContains, out via);
					if ((Object)(object)val != (Object)null)
					{
						m.m_merchantInventoryTablePrefab = val;
						try
						{
							m.InitDropTableGameObject();
						}
						catch (Exception ex)
						{
							Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "': deferred stock-table build threw: " + ex.Message));
						}
						Plugin.Log.LogMessage((object)("[STORYKIT] '" + spec.Id + "': stock table '" + ((Object)val).name + "' resolved off the spawn frame via " + via + "."));
					}
				}
				try
				{
					bool filled;
					int num = RefreshAndMaybeFillFallback(spec, m, out filled);
					Plugin.Log.LogMessage((object)$"[STORYKIT] '{spec.Id}': shop stock ready — {num} item stack(s) in the pouch.");
				}
				catch (Exception arg)
				{
					Plugin.Log.LogWarning((object)$"[STORYKIT] '{spec.Id}': first stock roll threw: {arg}");
				}
			}
		}

		internal static int RefreshAndMaybeFillFallback(NpcSpec spec, Merchant m, out bool filled)
		{
			filled = false;
			if ((Object)(object)m == (Object)null || (Object)(object)m.MerchantPouch == (Object)null)
			{
				return 0;
			}
			ItemContainer merchantPouch = m.MerchantPouch;
			MerchantPouch val = (MerchantPouch)(object)((merchantPouch is MerchantPouch) ? merchantPouch : null);
			if ((Object)(object)val == (Object)null)
			{
				m.RefreshInventory();
				return m.MerchantPouch.ItemCount;
			}
			double nextRefreshTime = val.m_nextRefreshTime;
			m.RefreshInventory();
			double nextRefreshTime2 = val.m_nextRefreshTime;
			int itemCount = ((ItemContainer)val).ItemCount;
			if (!MerchantRefreshRules.ShouldFillFallback(nextRefreshTime, nextRefreshTime2, itemCount))
			{
				if (itemCount == 0)
				{
					Plugin.Log.LogMessage((object)("[STORYKIT] '" + spec.Id + "': shop is empty but the restock clock has not come round " + $"(next refresh at game-time {nextRefreshTime2:0.##}, now {EnvironmentConditions.GameTime:0.##}) — fallback stock NOT minted."));
				}
				return itemCount;
			}
			int num = FillFallback(spec, m);
			if (num > 0)
			{
				filled = true;
				float num2 = ((spec.Merchant != null) ? spec.Merchant.RefreshRateGameHours : 0f);
				val.m_nextRefreshTime = MerchantRefreshRules.StampAfterFallback(EnvironmentConditions.GameTime, num2, nextRefreshTime2);
			}
			return ((ItemContainer)val).ItemCount;
		}

		internal static int FillFallback(NpcSpec spec, Merchant m)
		{
			List<string> list = ((spec.Merchant != null) ? spec.Merchant.FallbackItemNames : null);
			if (list == null || list.Count == 0 || (Object)(object)m.MerchantPouch == (Object)null)
			{
				return 0;
			}
			int num = 0;
			foreach (string item in list)
			{
				if (!ItemNames.TryResolveLoose(item, out var id))
				{
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "': fallback stock item '" + item + "' matches no item display name — skipped."));
				}
				else
				{
					Item val = ItemManager.Instance.GenerateItemNetwork(id);
					if (!((Object)(object)val == (Object)null))
					{
						val.ChangeParent(((Component)m.MerchantPouch).transform);
						num++;
					}
				}
			}
			return num;
		}

		internal static void ClearStockTableCache()
		{
			_tableCache.Clear();
			_tableCacheVia.Clear();
		}

		internal static Transform FindStockTable(string contains, out string via, bool includeAssets = true)
		{
			//IL_013c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0141: Unknown result type (might be due to invalid IL or missing references)
			via = null;
			if (string.IsNullOrEmpty(contains))
			{
				return null;
			}
			string key = SceneManagerHelper.ActiveSceneName + "\0" + contains + "\0" + includeAssets;
			if (_tableCache.TryGetValue(key, out var value))
			{
				if ((Object)(object)value != (Object)null)
				{
					via = (_tableCacheVia.TryGetValue(key, out var value2) ? (value2 + " (cached)") : "cached");
					return value;
				}
				if (value == null)
				{
					via = null;
					return null;
				}
				_tableCache.Remove(key);
				_tableCacheVia.Remove(key);
			}
			Transform val = null;
			Merchant[] array = Object.FindObjectsOfType<Merchant>();
			foreach (Merchant val2 in array)
			{
				Transform val3 = (((Object)(object)val2 != (Object)null) ? val2.m_merchantInventoryTablePrefab : null);
				if ((Object)(object)val3 != (Object)null && ((Object)val3).name.IndexOf(contains, StringComparison.OrdinalIgnoreCase) >= 0)
				{
					via = "live merchant '" + ((Object)val2).name + "'";
					val = val3;
					break;
				}
			}
			if ((Object)(object)val == (Object)null && includeAssets)
			{
				Dropable[] array2 = Resources.FindObjectsOfTypeAll<Dropable>();
				foreach (Dropable val4 in array2)
				{
					if (!((Object)(object)val4 == (Object)null))
					{
						Scene scene = ((Component)val4).gameObject.scene;
						if (!((Scene)(ref scene)).IsValid() && ((Object)val4).name.IndexOf(contains, StringComparison.OrdinalIgnoreCase) >= 0)
						{
							via = "loaded Dropable asset";
							val = ((Component)val4).transform;
							break;
						}
					}
				}
			}
			_tableCache[key] = val;
			_tableCacheVia[key] = via;
			return val;
		}
	}
	internal static class ItemNames
	{
		internal static bool TryResolveLoose(string name, out int id)
		{
			id = 0;
			if (string.IsNullOrEmpty(name))
			{
				return false;
			}
			if (ItemNameIndex.TryResolve(name, ref id))
			{
				return true;
			}
			string text = ((name.IndexOf('’') >= 0) ? name.Replace('’', '\'') : name.Replace('\'', '’'));
			if (!string.Equals(text, name, StringComparison.Ordinal) && ItemNameIndex.TryResolve(text, ref id))
			{
				return true;
			}
			string text2 = default(string);
			if (!ItemNameIndex.TryResolveCatalog(name, ref id, ref text2))
			{
				return ItemNameIndex.TryResolveCatalog(text, ref id, ref text2);
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(AISWander), "Update")]
	public static class NpcDialogueHold
	{
		public static bool Enabled = true;

		private const float TurnDegreesPerFrame = 6f;

		private static readonly HashSet<int> _held = new HashSet<int>();

		public static int Count => _held.Count;

		public static bool IsHolding(Character c)
		{
			if ((Object)(object)c == (Object)null || !_held.Contains(((Object)c).GetInstanceID()))
			{
				return false;
			}
			try
			{
				return DialogueHold.ShouldHold(true, NpcRegistry.IsBodyInDialogue(c));
			}
			catch
			{
				return true;
			}
		}

		public static void Watch(Character c)
		{
			if (!((Object)(object)c == (Object)null))
			{
				_held.Add(((Object)c).GetInstanceID());
			}
		}

		public static void Forget(Character c)
		{
			if (!((Object)(object)c == (Object)null))
			{
				_held.Remove(((Object)c).GetInstanceID());
			}
		}

		[HarmonyPrefix]
		private static bool Prefix(AISWander __instance)
		{
			if (!Enabled || _held.Count == 0)
			{
				return true;
			}
			if ((Object)(object)__instance == (Object)null)
			{
				return true;
			}
			Character character = ((AIState)__instance).m_character;
			if ((Object)(object)character == (Object)null || !_held.Contains(((Object)character).GetInstanceID()))
			{
				return true;
			}
			bool flag;
			try
			{
				flag = NpcRegistry.IsBodyInDialogue(character);
			}
			catch
			{
				flag = true;
			}
			if (!DialogueHold.ShouldHold(true, flag))
			{
				return true;
			}
			CharacterAI characterAI = ((AIState)__instance).m_characterAI;
			if ((Object)(object)characterAI != (Object)null)
			{
				characterAI.StopMovement();
				characterAI.SpeedModif = 0f;
			}
			FacePlayer(character);
			return false;
		}

		private static void FacePlayer(Character c)
		{
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: 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_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Unknown result type (might be due to invalid IL or missing references)
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0061: Unknown result type (might be due to invalid IL or missing references)
			if (TryNearestPlayerPos(c, out var pos))
			{
				Vector3 position = ((Component)c).transform.position;
				float num = default(float);
				if (DialogueHold.TryFaceYaw(position.x, position.z, pos.x, pos.z, ref num))
				{
					Quaternion val = Quaternion.Euler(0f, num, 0f);
					((Component)c).transform.rotation = Quaternion.RotateTowards(((Component)c).transform.rotation, val, 6f);
				}
			}
		}

		private static bool TryNearestPlayerPos(Character npc, out Vector3 pos)
		{
			//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_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Unknown result type (might be due to invalid IL or missing references)
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_0071: Unknown result type (might be due to invalid IL or missing references)
			//IL_008d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0092: Unknown result type (might be due to invalid IL or missing references)
			pos = Vector3.zero;
			if ((Object)(object)Global.Lobby == (Object)null)
			{
				return false;
			}
			float num = float.MaxValue;
			Vector3 position = ((Component)npc).transform.position;
			foreach (PlayerSystem item in Global.Lobby.PlayersInLobby)
			{
				Character val = (((Object)(object)item != (Object)null) ? item.ControlledCharacter : null);
				if (!((Object)(object)val == (Object)null))
				{
					float num2 = Vector3.SqrMagnitude(((Component)val).transform.position - position);
					if (num2 < num)
					{
						num = num2;
						pos = ((Component)val).transform.position;
					}
				}
			}
			return num < float.MaxValue;
		}
	}
	internal static class NpcDirector
	{
		private static readonly HashSet<string> _skipReported = new HashSet<string>();

		private static readonly RigFailurePolicy _specFailures = new RigFailurePolicy();

		private static string _lastReassertScene;

		private static bool _lastReassertSceneKnown;

		private const float ConvergeSeconds = 3f;

		private static float _nextConverge;

		private static readonly RoomRoleWatch _roomWatch = new RoomRoleWatch();

		private static readonly HashSet<string> _noTemplateWarned = new HashSet<string>();

		private const float NoReplicaWarnSeconds = 60f;

		private static readonly Dictionary<string, float> _noReplicaWarned = new Dictionary<string, float>();

		internal static bool IsStamped(Placement p)
		{
			if (p != null)
			{
				if (p.X == 0f && p.Y == 0f)
				{
					return p.Z != 0f;
				}
				return true;
			}
			return false;
		}

		private static void NoteSpecFailure(string id, string what, Exception e)
		{
			//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_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Invalid comparison between Unknown and I4
			FailureVerdict val = _specFailures.RecordFailure(id);
			if ((int)val != 0)
			{
				if ((int)val == 2)
				{
					Plugin.Log.LogError((object)($"[STORYKIT] {what} for '{id}' has now thrown {_specFailures.MaxAttempts} times " + "— GIVING UP on this spec (no further attempts on the convergence tick). A SCENE CHANGE or a room change re-arms it. Last: " + e.Message));
					return;
				}
				Plugin.Log.LogWarning((object)$"[STORYKIT] {what} for '{id}' threw again (#{_specFailures.FailureCount(id)}/{_specFailures.MaxAttempts}): {e.Message}");
			}
			else
			{
				Plugin.Log.LogWarning((object)$"[STORYKIT] {what} for '{id}' threw — skipping this spec and continuing the walk: {e}");
			}
		}

		private static void ReArmOnSceneChange(string sceneName)
		{
			if (_lastReassertSceneKnown && string.Equals(_lastReassertScene, sceneName, StringComparison.OrdinalIgnoreCase))
			{
				return;
			}
			if (_lastReassertSceneKnown)
			{
				foreach (string cappedId in _specFailures.CappedIds)
				{
					Plugin.Log.LogMessage((object)("[STORYKIT] scene changed ('" + (_lastReassertScene ?? "none") + "' → '" + (sceneName ?? "none") + "') — re-arming the given-up spec '" + cappedId + "'."));
				}
				_specFailures.Reset();
			}
			_lastReassertScene = sceneName;
			_lastReassertSceneKnown = true;
		}

		internal static void Reassert(string sceneName)
		{
			if (!Plugin.EnableStory.Value)
			{
				return;
			}
			ReArmOnSceneChange(sceneName);
			if (PhotonNetwork.isNonMasterClientInRoom)
			{
				EnsureGuestRigs();
				return;
			}
			foreach (NpcSpec item in NpcRegistry.Specs())
			{
				if (_specFailures.ShouldAttempt(item.Id))
				{
					try
					{
						ReassertOne(item, sceneName);
						_specFailures.RecordSuccess(item.Id);
					}
					catch (Exception e)
					{
						NoteSpecFailure(item.Id, "placement", e);
					}
				}
			}
		}

		private static void ReassertOne(NpcSpec spec, string sceneName)
		{
			if (spec.Placements == null || spec.Placements.Count == 0)
			{
				return;
			}
			Placement val = spec.Placements[0];
			if (!string.Equals(val.Scene, sceneName, StringComparison.OrdinalIgnoreCase))
			{
				return;
			}
			if (!IsStamped(val))
			{
				Plugin.Log.LogMessage((object)("[STORYKIT] '" + spec.Id + "' is placed in '" + sceneName + "' but its position is unstamped (0,0,0) — stand at the intended spot and stamp it (e.g. BW's 'marenhere'), or force-spawn beside you with 'storynpcspawn " + spec.Id + " here'."));
			}
			else
			{
				if (NpcRegistry.IsLive(spec.Id))
				{
					return;
				}
				if (!NpcRegistry.IsSpawnable(spec.Id))
				{
					if (_skipReported.Add(spec.Id))
					{
						Plugin.Log.LogWarning((object)("[STORYKIT] NOT placing '" + spec.Id + "' in '" + sceneName + "': " + (NpcRegistry.UnspawnableReason(spec.Id) ?? "not spawnable") + ". (Said once per id per session; 'storynpclist' shows the live state. A 'template not built yet' here means this scene load ran before the SL pack-load build — the convergence tick places her when it lands.)"));
					}
				}
				else
				{
					Plugin.Log.LogMessage((object)$"[STORYKIT] placing '{spec.Id}' in '{sceneName}' at ({val.X:F1},{val.Y:F1},{val.Z:F1}).");
					NpcRegistry.TrySpawn(spec.Id);
				}
			}
		}

		internal static void Tick()
		{
			//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)
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Invalid comparison between Unknown and I4
			//IL_004e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0050: Invalid comparison between Unknown and I4
			if (!Plugin.EnableStory.Value)
			{
				return;
			}
			string text = ((PhotonNetwork.inRoom && PhotonNetwork.room != null) ? PhotonNetwork.room.Name : null);
			bool flag = !PhotonNetwork.isNonMasterClientInRoom;
			string room = _roomWatch.Room;
			RoomRoleEvent val = _roomWatch.Observe(text, flag);
			if ((int)val != 2)
			{
				if ((int)val == 3)
				{
					Plugin.Log.LogMessage((object)("[STORYKIT] master migration in room '" + (text ?? "none") + "' — this peer is now " + (flag ? "MASTER (promoted)" : "guest (demoted)") + ": dropping live beliefs, re-arming failures, abandoning in-flight spawns and sweeping departed-host orphans."));
					NpcRegistry.ForgetLive();
					_specFailures.Reset();
					NpcRegistry.AbandonInFlight("master migration");
					_nextConverge = 0f;
					if (flag)
					{
						SweepDepartedHostOrphans();
					}
				}
			}
			else
			{
				Plugin.Log.LogMessage((object)("[STORYKIT] room changed ('" + (room ?? "none") + "' → '" + (text ?? "none") + "') — re-evaluating placements now (" + RoleWord() + ")."));
				NpcRegistry.ForgetLive();
				_specFailures.Reset();
				NpcRegistry.AbandonInFlight("room changed");
				_nextConverge = 0f;
			}
			if (!(Time.unscaledTime < _nextConverge))
			{
				_nextConverge = Time.unscaledTime + 3f;
				DialogueBuilder.FlushStashedOrphans();
				Converge();
			}
		}

		private static void Converge()
		{
			if (!IsGameplayResumed())
			{
				return;
			}
			if (PhotonNetwork.isNonMasterClientInRoom)
			{
				EnsureGuestRigs();
				return;
			}
			string activeSceneName = SceneManagerHelper.ActiveSceneName;
			string text = FirstMissingIn(activeSceneName);
			if (text != null)
			{
				Plugin.Log.LogMessage((object)("[STORYKIT] '" + text + "' is absent from the character registry — convergent respawn in '" + activeSceneName + "'."));
				Reassert(activeSceneName);
			}
		}

		private static string FirstMissingIn(string sceneName)
		{
			foreach (NpcSpec item in NpcRegistry.Specs())
			{
				if (PlacedIn(item, sceneName) && NpcRegistry.IsSpawnable(item.Id) && !NpcRegistry.IsLive(item.Id))
				{
					return item.Id;
				}
			}
			return null;
		}

		private static bool PlacedIn(NpcSpec spec, string sceneName)
		{
			if (spec.Placements == null || spec.Placements.Count == 0)
			{
				return false;
			}
			Placement val = spec.Placements[0];
			if (string.Equals(val.Scene, sceneName, StringComparison.OrdinalIgnoreCase))
			{
				return IsStamped(val);
			}
			return false;
		}

		private static string RoleWord()
		{
			if (!PhotonNetwork.isNonMasterClientInRoom)
			{
				return "master";
			}
			return "guest — the master places NPCs";
		}

		internal static bool IsGameplayResumed()
		{
			return Lifecycle.IsGameplayLive();
		}

		internal static IEnumerator ReassertWhenResumed(string sceneName)
		{
			float t0 = Time.unscaledTime;
			while (Time.unscaledTime - t0 < 30f && !IsGameplayResumed())
			{
				yield return (object)new WaitForSecondsRealtime(0.25f);
			}
			if (!string.Equals(SceneManagerHelper.ActiveSceneName, sceneName, StringComparison.OrdinalIgnoreCase))
			{
				Plugin.Log.LogMessage((object)("[STORYKIT] scene changed during defer, abandoning spawn for '" + sceneName + "' (now in '" + SceneManagerHelper.ActiveSceneName + "')."));
				yield break;
			}
			if (!IsGameplayResumed())
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] gameplay did not resume within 30s in '" + sceneName + "' — re-asserting placements anyway (" + RoleWord() + "; the visuals backstop will catch a bare body)."));
			}
			else
			{
				Plugin.Log.LogMessage((object)("[STORYKIT] gameplay resumed in '" + sceneName + "' — re-asserting placements (" + RoleWord() + ")."));
			}
			Reassert(sceneName);
		}

		private static void EnsureGuestRigs()
		{
			if ((Object)(object)CharacterManager.Instance == (Object)null)
			{
				return;
			}
			foreach (NpcSpec item in NpcRegistry.Specs())
			{
				if (_specFailures.ShouldAttempt(item.Id))
				{
					try
					{
						EnsureGuestRig(item);
						_specFailures.RecordSuccess(item.Id);
					}
					catch (Exception e)
					{
						NoteSpecFailure(item.Id, "guest rig", e);
					}
				}
			}
			WarnUnmatchedTemplateBodies();
		}

		private static void EnsureGuestRig(NpcSpec spec)
		{
			Character val = NpcRegistry.Resolve(spec.Id);
			if (!NpcRegistry.IsSpawnable(spec.Id))
			{
				return;
			}
			if ((Object)(object)val == (Object)null)
			{
				if (PlacedIn(spec, SceneManagerHelper.ActiveSceneName))
				{
					WarnNoReplica(spec.Id);
				}
				return;
			}
			bool flag = _specFailures.FailureCount(spec.Id) > 0;
			if (flag || !((Object)(object)((Component)val).GetComponent<NpcLookFollow>() != (Object)null))
			{
				Plugin.Log.LogMessage((object)("[STORYKIT] '" + spec.Id + "': replicated NPC found un-rigged on this guest — rigging" + (flag ? $" (retry #{_specFailures.FailureCount(spec.Id) + 1} after a failed rig)." : ".")));
				NpcRegistry.RigCharacter(spec, val, "replica rigged (guest walk)");
			}
		}

		private static void WarnUnmatchedTemplateBodies()
		{
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if ((Object)(object)CharacterManager.Instance == (Object)null)
				{
					return;
				}
				DictionaryExt<string, Character> characters = CharacterManager.Instance.Characters;
				if (characters == null)
				{
					return;
				}
				for (int i = 0; i < characters.Count; i++)
				{
					Character val = characters.Values[i];
					if (!((Object)(object)val == (Object)null) && Object.op_Implicit((Object)(object)val))
					{
						string text = UID.op_Implicit(val.UID);
						if (!string.IsNullOrEmpty(text) && ((Object)val).name.EndsWith("_" + text, StringComparison.Ordinal) && !CustomCharacters.Templates.ContainsKey(text) && NpcRegistry.Find(text) == null && _noTemplateWarned.Add(text))
						{
							Plugin.Log.LogWarning((object)("[STORYKIT] replicated SideLoader body '" + ((Object)val).name + "' (uid '" + text + "') has NO template on this machine — it will stay bare (no equipment/AI/stats/dialogue/shop). This is the dynamically-minted-spec gap: the peer that minted the spec registered it only there. Said once per uid per session."));
						}
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] unmatched-template sweep threw: " + ex.Message));
			}
		}

		private static void SweepDepartedHostOrphans()
		{
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			//IL_0118: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ef: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if ((Object)(object)CharacterManager.Instance == (Object)null)
				{
					return;
				}
				DictionaryExt<string, Character> characters = CharacterManager.Instance.Characters;
				if (characters == null)
				{
					return;
				}
				List<Character> list = new List<Character>();
				for (int i = 0; i < characters.Count; i++)
				{
					Character val = characters.Values[i];
					if ((Object)(object)val == (Object)null || !Object.op_Implicit((Object)(object)val))
					{
						continue;
					}
					string text = UID.op_Implicit(val.UID);
					if (!string.IsNullOrEmpty(text))
					{
						bool flag = ((Object)val).name.EndsWith("_" + text, StringComparison.Ordinal);
						bool flag2 = CustomCharacters.Templates.ContainsKey(text);
						bool flag3 = NpcRegistry.Find(text) != null;
						if (OrphanSweepRules.ShouldSweep(flag, flag2, flag3, text))
						{
							list.Add(val);
						}
					}
				}
				foreach (Character item in list)
				{
					if (NpcRegistry.IsBodyInDialogue(item))
					{
						Plugin.Log.LogWarning((object)$"[STORYKIT] departed-host orphan '{((Object)item).name}' (uid '{item.UID}') is mid-dialogue — left standing (despawn it by hand once the conversation ends; it dies with the scene otherwise).");
						continue;
					}
					Plugin.Log.LogWarning((object)$"[STORYKIT] departed-host orphan '{((Object)item).name}' (uid '{item.UID}') — a minted spec's body whose owner left the room; despawning it.");
					NpcRegistry.DespawnForeignBody(item);
				}
				if (list.Count == 0)
				{
					Plugin.Log.LogMessage((object)"[STORYKIT] master-migration orphan sweep: nothing to sweep.");
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] departed-host orphan sweep threw: " + ex.Message));
			}
		}

		private static void WarnNoReplica(string id)
		{
			string key = id + "|" + SceneManagerHelper.ActiveSceneName;
			if (!_noReplicaWarned.TryGetValue(key, out var value) || !(Time.unscaledTime - value < 60f))
			{
				_noReplicaWarned[key] = Time.unscaledTime;
				Plugin.Log.LogWarning((object)("[STORYKIT] no replica for '" + id + "' on this guest — the master hasn't spawned it (or replication missed)."));
			}
		}
	}
	public class NpcLookFollow : MonoBehaviour
	{
		private const float LookRange = 5f;

		private const float ScanSeconds = 0.5f;

		private const float DegreesPerFrame = 1f;

		private float _lastScan = -1f;

		private Vector3 _lookPosition = Vector3.zero;

		private float _closestDistance = float.MaxValue;

		internal void Update()
		{
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			//IL_0069: Unknown result type (might be due to invalid IL or missing references)
			//IL_006e: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			if (Time.time - _lastScan > 0.5f)
			{
				_lastScan = Time.time;
				UpdateLookTarget();
			}
			if (_closestDistance < 5f)
			{
				Vector3 val = _lookPosition - ((Component)this).transform.position;
				val.y = 0f;
				if (!(((Vector3)(ref val)).sqrMagnitude < 0.01f))
				{
					Quaternion val2 = Quaternion.LookRotation(val, Vector3.up);
					((Component)this).transform.rotation = Quaternion.RotateTowards(((Component)this).transform.rotation, val2, 1f);
				}
			}
		}

		private void UpdateLookTarget()
		{
			//IL_0055: 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_0083: Unknown result type (might be due to invalid IL or missing references)
			//IL_0088: Unknown result type (might be due to invalid IL or missing references)
			_closestDistance = float.MaxValue;
			if ((Object)(object)Global.Lobby == (Object)null)
			{
				return;
			}
			foreach (PlayerSystem item in Global.Lobby.PlayersInLobby)
			{
				Character val = (((Object)(object)item != (Object)null) ? item.ControlledCharacter : null);
				if (!((Object)(object)val == (Object)null))
				{
					float num = Vector3.Distance(((Component)this).transform.position, ((Component)val).transform.position);
					if (num < 5f && num < _closestDistance)
					{
						_lookPosition = ((Component)val).transform.position;
						_closestDistance = num;
					}
				}
			}
		}
	}
	public class NpcPin : MonoBehaviour
	{
		public Vector3 Home;

		private const float EpsilonSqr = 0.0004f;

		private float _lastShoveWarn = -999f;

		private const int SettledFramesNeeded = 10;

		private const float ArmTimeoutSeconds = 10f;

		private bool _armed = true;

		private int _settledFrames;

		private Vector3 _lastSeen;

		private float _armDeadline;

		internal void ArmWhenSettled()
		{
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_001a: Unknown result type (might be due to invalid IL or missing references)
			_armed = false;
			_settledFrames = 0;
			_lastSeen = ((Component)this).transform.position;
			_armDeadline = Time.unscaledTime + 10f;
		}

		internal void LateUpdate()
		{
			//IL_00cd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00dd: 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_0016: 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_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0151: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			//IL_004b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0136: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			//IL_0078: Unknown result type (might be due to invalid IL or missing references)
			//IL_008e: Unknown result type (might be due to invalid IL or missing references)
			if (!_armed)
			{
				Vector3 position = ((Component)this).transform.position;
				Vector3 val = position - _lastSeen;
				if (((Vector3)(ref val)).sqrMagnitude <= 0.0004f)
				{
					_settledFrames++;
				}
				else
				{
					_settledFrames = 0;
				}
				_lastSeen = position;
				bool flag = Time.unscaledTime >= _armDeadline;
				if (_settledFrames >= 10 || flag)
				{
					_armed = true;
					Home = position;
					Plugin.Log.LogMessage((object)($"[STORYKIT] pin: '{((Object)this).name}' armed at {Home}" + (flag ? " (settle TIMED OUT — the replica never stopped moving; pinning where it is)" : $" after {_settledFrames} settled frames (guest replica: waited for the position sync).")));
				}
				return;
			}
			Vector3 val2 = ((Component)this).transform.position - Home;
			if (!(((Vector3)(ref val2)).sqrMagnitude <= 0.0004f))
			{
				if (((Vector3)(ref val2)).sqrMagnitude > 0.25f && Time.unscaledTime - _lastShoveWarn > 30f)
				{
					_lastShoveWarn = Time.unscaledTime;
					Plugin.Log.LogMessage((object)$"[STORYKIT] pin: '{((Object)this).name}' was displaced {((Vector3)(ref val2)).magnitude:F2}m from home {Home} — snapping back (something is shoving this NPC).");
				}
				((Component)this).transform.position = Home;
			}
		}
	}
	public static class NpcRegistry
	{
		private sealed class Entry
		{
			public NpcSpec Spec;

			public StoryNpcTemplate Template;

			public Character Live;

			public int SpawnCount;

			public string RefusedReason;
		}

		public enum SpawnPolicy
		{
			Refuse,
			Defer
		}

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

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

		private static bool _templatesBuilt;

		private static readonly int s_sessionSalt = Environment.TickCount;

		private static readonly FieldInfo s_disableAIRoot = typeof(CharacterAI).GetField("m_disableAIRoot", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

		private static readonly FieldInfo s_checkQuestEvent = typeof(CharacterAI).GetField("m_checkQuestEvent", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

		private static readonly FieldInfo s_aiActiveOnQuestEvent = typeof(CharacterAI).GetField("m_aiActiveOnQuestEvent", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

		private static readonly HashSet<string> _deferred = new HashSet<string>(StringComparer.Ordinal);

		private static int _inFlightEpoch;

		internal const float LocalShopOpenGraceSeconds = 3f;

		internal const float RemoteShopGraceSeconds = 300f;

		private static readonly Dictionary<int, KeyValuePair<int, float>> _buyerSeen = new Dictionary<int, KeyValuePair<int, float>>();

		private static readonly HashSet<long> _staleWarned = new HashSet<long>();

		public static bool TemplatesBuilt => _templatesBuilt;

		public static int SpecCount => _order.Count;

		public static void Register(NpcSpec spec)
		{
			if (spec == null || string.IsNullOrEmpty(spec.Id))
			{
				Plugin.Log.LogWarning((object)"[STORYKIT] Register: spec null or missing Id — ignored.");
				return;
			}
			if (_entries.ContainsKey(spec.Id))
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] Register: duplicate NPC id '" + spec.Id + "' — first registration wins."));
				return;
			}
			Entry entry = new Entry
			{
				Spec = spec
			};
			_entries[spec.Id] = entry;
			_order.Add(spec.Id);
			Plugin.Log.LogMessage((object)("[STORYKIT] NPC spec registered: '" + spec.Id + "' ('" + spec.Name + "')."));
			if (!_templatesBuilt)
			{
				return;
			}
			try
			{
				BuildOne(entry);
			}
			catch (Exception ex)
			{
				entry.RefusedReason = "exception: " + ex.Message;
				Plugin.Log.LogWarning((object)$"[STORYKIT] late template build FAILED for '{spec.Id}': {ex}");
			}
		}

		public static NpcSpec Find(string id)
		{
			if (!_entries.TryGetValue(id ?? "", out var value))
			{
				return null;
			}
			return value.Spec;
		}

		internal static void BuildTemplates()
		{
			_templatesBuilt = true;
			string[] array = _order.ToArray();
			foreach (string text in array)
			{
				Entry entry = _entries[text];
				try
				{
					BuildOne(entry);
				}
				catch (Exception ex)
				{
					entry.RefusedReason = "exception: " + ex.Message;
					Plugin.Log.LogWarning((object)$"[STORYKIT] template build FAILED for '{text}': {ex}");
				}
			}
		}

		private static void BuildOne(Entry e)
		{
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0251: Unknown result type (might be due to invalid IL or missing references)
			//IL_0257: Unknown result type (might be due to invalid IL or missing references)
			//IL_0264: Unknown result type (might be due to invalid IL or missing references)
			//IL_026e: Expected O, but got Unknown
			//IL_00cf: 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_0159: Invalid comparison between Unknown and I4
			NpcSpec spec = e.Spec;
			List<SpecIssue> list = SpecValidation.Validate(spec);
			foreach (SpecIssue item in list)
			{
				if ((int)item.Severity == 0)
				{
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "': " + item.Message));
				}
			}
			string text = SpecValidation.FirstError(list);
			if (text != null)
			{
				Refuse(e, text);
				return;
			}
			bool flag = spec.Trainer != null;
			int num = 0;
			if (flag)
			{
				List<TreeIssue> list2 = TreeLayout.Validate(spec.Trainer.Tree);
				foreach (TreeIssue item2 in list2)
				{
					if ((int)item2.Severity == 0)
					{
						Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "' tree: " + item2.Message));
					}
				}
				if (TreeLayout.HasErrors((IEnumerable<TreeIssue>)list2))
				{
					StringBuilder stringBuilder = new StringBuilder("[STORYKIT] '" + spec.Id + "' tree REFUSED — layout errors:");
					foreach (TreeIssue item3 in list2)
					{
						if ((int)item3.Severity == 1)
						{
							stringBuilder.Append("\n  ").Append(item3.Message);
						}
					}
					Plugin.Log.LogWarning((object)stringBuilder.ToString());
					Refuse(e, "tree layout errors (see boot log)");
					return;
				}
				num = list2.Count;
			}
			Factions f = (Factions)0;
			if (!string.IsNullOrEmpty(spec.Faction) && !TryParseFaction(spec.Faction, out f))
			{
				Refuse(e, "unknown Faction '" + spec.Faction + "' (Character.Factions names: " + string.Join(", ", Enum.GetNames(typeof(Factions))) + ")");
				return;
			}
			StoryNpcTemplate storyNpcTemplate = new StoryNpcTemplate
			{
				Spec = spec,
				UID = spec.Id,
				Name = spec.Name,
				SaveType = (CharSaveType)0,
				Faction = f,
				CharacterVisualsData = new VisualData(),
				KeepStats = (spec.Mobile || spec.Combat != null),
				LootableOnDeath = false,
				DropPouchContents = false,
				DropWeapons = false,
				InitialDialogue = ((spec.Dialogue.Greetings.Count > 0) ? spec.Dialogue.Greetings[0] : "..."),
				SkillTree = (flag ? TrainerWiring.BuildTree(spec.Trainer) : null)
			};
			if (spec.WeaponId != 0)
			{
				((SL_Character)storyNpcTemplate).Weapon_ID = spec.WeaponId;
			}
			if (spec.HelmetId != 0)
			{
				((SL_Character)storyNpcTemplate).Helmet_ID = spec.HelmetId;
			}
			if (spec.ChestId != 0)
			{
				((SL_Character)storyNpcTemplate).Chest_ID = spec.ChestId;
			}
			if (spec.BootsId != 0)
			{
				((SL_Character)storyNpcTemplate).Boots_ID = spec.BootsId;
			}
			if (spec.ShieldId != 0)
			{
				((SL_Character)storyNpcTemplate).Shield_ID = spec.ShieldId;
			}
			if (spec.BackpackId != 0)
			{
				((SL_Character)storyNpcTemplate).Backpack_ID = spec.BackpackId;
			}
			ApplyRandomLooks(spec, storyNpcTemplate);
			ApplyExplicitLooks(spec, storyNpcTemplate);
			string text2 = ApplyMobileAxis(spec, storyNpcTemplate);
			if (text2 != null)
			{
				Refuse(e, text2);
				return;
			}
			((SL_Character)storyNpcTemplate).OnSpawn += delegate(Character c, string rpcData)
			{
				OnSpawned(e, c, rpcData);
			};
			((ContentTemplate)storyNpcTemplate).ApplyTemplate();
			SL_Character value;
			SL_Character val = (CustomCharacters.Templates.TryGetValue(spec.Id, out value) ? value : null);
			if ((object)val != storyNpcTemplate)
			{
				Refuse(e, (val == null) ? "ApplyTemplate did not register the template (SideLoader refused it — see the SL error above)" : ("SideLoader's template for UID '" + spec.Id + "' belongs to someone else ('" + val.Name + "') — a duplicate UID; this NPC would spawn un-rigged"));
				return;
			}
			if (flag)
			{
				StampTreeIcons(spec);
			}
			e.Template = storyNpcTemplate;
			Plugin.Log.LogMessage((object)(flag ? $"[STORYKIT] trainer template '{spec.Id}' applied (tree '{spec.Trainer.SkillTreeUID}': {spec.Trainer.Tree.Slots.Count} slots, {num} layout warns)." : $"[STORYKIT] plain-dialogue template '{spec.Id}' applied (no trainer; {spec.Dialogue.Choices.Count} dialogue choices)."));
		}

		private static void Refuse(Entry e, string why)
		{
			e.RefusedReason = why;
			Plugin.Log.LogWarning((object)("[STORYKIT] NPC '" + e.Spec.Id + "' REFUSED: " + why + "."));
		}

		private static bool TryParseFaction(string name, out Factions f)
		{
			if (Enum.TryParse<Factions>(name.Trim(), ignoreCase: true, out f) && (int)f != 10)
			{
				return true;
			}
			f = (Factions)0;
			return false;
		}

		private static void ApplyRandomLooks(NpcSpec spec, StoryNpcTemplate template)
		{
			//IL_00bd: Unknown result type (might be due to invalid IL or missing references)
			//IL_0177: Unknown result type (might be due to invalid IL or missing references)
			//IL_0179: Unknown result type (might be due to invalid IL or missing references)
			//IL_017c: Unknown result type (might be due to invalid IL or missing references)
			//IL_017e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0183: Unknown result type (might be due to invalid IL or missing references)
			//IL_0186: Unknown result type (might be due to invalid IL or missing references)
			//IL_018b: Unknown result type (might be due to invalid IL or missing references)
			//IL_018c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0193: Unknown result type (might be due to invalid IL or missing references)
			//IL_0198: Unknown result type (might be due to invalid IL or missing references)
			//IL_0199: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bf: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d1: Expected O, but got Unknown
			//IL_01e1: Unknown result type (might be due to invalid IL or missing references)
			bool flag = spec.OutfitPool != null && spec.OutfitPool.Count > 0;
			if (!spec.RandomVisuals && !flag)
			{
				return;
			}
			int num = VisualRoll.Seed(spec.Id, s_sessionSalt);
			if (spec.RandomVisuals)
			{
				CharacterVisualsPresets val = null;
				try
				{
					val = CharacterManager.CharacterVisualsPresets;
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "': CharacterVisualsPresets threw (" + ex.Message + ")."));
				}
				if ((Object)(object)val == (Object)null)
				{
					Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "': RandomVisuals skipped — CharacterVisualsPresets not loadable (SideLoader default look)."));
				}
				else
				{
					VisualBounds val2 = new VisualBounds
					{
						Skins = Len(val.MSkins),
						FemaleSkins = Len(val.FSkins),
						MaleHeadsWhite = Len(val.MHeadsWhite),
						MaleHeadsBlack = Len(val.MHeadsBlack),
						MaleHeadsAsian = Len(val.MHeadsAsian),
						FemaleHeadsWhite = Len(val.FHeadsWhite),
						FemaleHeadsBlack = Len(val.FHeadsBlack),
						FemaleHeadsAsian = Len(val.FHeadsAsian),
						Hairs = Len(val.Hairs),
						HairColors = Len(val.HairMaterials)
					};
					VisualPick val3 = VisualRoll.Roll(num, val2);
					((SL_Character)template).CharacterVisualsData = new VisualData
					{
						Gender = (Gender)val3.Gender,
						SkinIndex = val3.SkinIndex,
						HeadVariationIndex = val3.HeadVariationIndex,
						HairStyleIndex = val3.HairStyleIndex,
						HairColorIndex = val3.HairColorIndex
					};
					Plugin.Log.LogMessage((object)$"[STORYKIT] '{spec.Id}': random visuals → {val3}.");
				}
			}
			if (!flag)
			{
				return;
			}
			OutfitSpec val4 = OutfitRoll.Pick((IList<OutfitSpec>)spec.OutfitPool, VisualRoll.Roll01(num, 1));
			if (val4 == null || val4.IsEmpty)
			{
				Plugin.Log.LogMessage((object)("[STORYKIT] '" + spec.Id + "': outfit roll picked an empty entry — explicit ids (or bare) stand."));
				return;
			}
			List<string> list = new List<string>();
			if (ResolvePiece(spec, val4.ChestName, "chest", out var id))
			{
				((SL_Character)template).Chest_ID = id;
				list.Add(val4.ChestName);
			}
			if (ResolvePiece(spec, val4.HelmetName, "helmet", out var id2))
			{
				((SL_Character)template).Helmet_ID = id2;
				list.Add(val4.HelmetName);
			}
			if (ResolvePiece(spec, val4.BootsName, "boots", out var id3))
			{
				((SL_Character)template).Boots_ID = id3;
				list.Add(val4.BootsName);
			}
			if (ResolvePiece(spec, val4.WeaponName, "weapon", out var id4))
			{
				((SL_Character)template).Weapon_ID = id4;
				list.Add(val4.WeaponName);
			}
			Plugin.Log.LogMessage((object)("[STORYKIT] '" + spec.Id + "': outfit → " + ((list.Count > 0) ? string.Join(" / ", list.ToArray()) : "nothing resolved") + "."));
		}

		private static int Len(Array a)
		{
			return a?.Length ?? 0;
		}

		private static void ApplyExplicitLooks(NpcSpec spec, StoryNpcTemplate template)
		{
			//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_0018: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0052: Expected O, but got Unknown
			VisualIndices visuals = spec.Visuals;
			if (visuals != null)
			{
				((SL_Character)template).CharacterVisualsData = new VisualData
				{
					Gender = (Gender)visuals.Gender,
					SkinIndex = visuals.SkinIndex,
					HeadVariationIndex = visuals.HeadVariationIndex,
					HairStyleIndex = visuals.HairStyleIndex,
					HairColorIndex = visuals.HairColorIndex
				};
				if (spec.RandomVisuals)
				{
					Plugin.Log.LogMessage((object)$"[STORYKIT] '{spec.Id}': explicit Visuals win over RandomVisuals ({visuals}).");
				}
			}
		}

		private static bool ResolvePiece(NpcSpec spec, string name, string slot, out int id)
		{
			id = 0;
			if (string.IsNullOrEmpty(name))
			{
				return false;
			}
			if (ItemNames.TryResolveLoose(name, out id))
			{
				return true;
			}
			Plugin.Log.LogWarning((object)("[STORYKIT] '" + spec.Id + "': outfit " + slot + " '" + name + "' matches no item display/prefab name on this install — piece skipped."));
			return false;
		}

		private static string ApplyMobileAxis(NpcSpec spec, StoryNpcTemplate template)
		{
			//IL_007f: 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_0090: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b6: 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)
			//IL_00cc: Expected O, but got Unknown
			//IL_0078: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ed: Unknown result type (might be due to invalid IL or missing references)
			if (!string.IsNullOrEmpty(spec.BackpackName))
			{
				if (!ItemNames.TryResolveLoose(spec.BackpackName, out var id))
				{
					return "BackpackName '" + spec.BackpackName + "' matches no item display/prefab name on this install";
				}
				((SL_Character)template).Backpack_ID = id;
				Plugin.Log.LogMessage((object)$"[STORYKIT] '{spec.Id}': backpack '{spec.BackpackName}' → ItemID {id}.");
			}
			if (!spec.Mobile)
			{
				return null;
			}
			AiSpec val = (AiSpec)(((object)spec.Ai) ?? ((object)new AiSpec()));
			((SL_Character)template).AI = (SL_CharacterAI)new SL_CharacterAIMelee
			{
				CanBlock = val.CanBlock,
				CanDodge = val.CanDodge,
				CanWanderFar = val.CanWanderFar,
				Wander_Speed = val.WanderSpeed,
				Wander_Type = (WanderType)0,
				Combat_ChanceToAttack = val.ChanceToAttack
			};
			CombatSpec combat = spec.Combat;
			if (combat != null)
			{
				if (combat.Health.HasValue)
				{
					((SL_Character)template).Health = combat.Health.Value;
				}
				if (combat.Protection.HasValue)
				{
					((SL_Character)template).Protection = combat.Protection.Value;
				}
				if (combat.DamageResists != null)
				{
					if (combat.DamageResists.Length != 6)
					{
						return "CombatSpec.DamageResists must have exactly 6 entries";
					}
					((SL_Character)template).Damage_Resists = (float[])combat.DamageResists.Clone();
				}
				float num = combat.DamageBonusMult - 1f;
				((SL_Character)template).Damage_Bonus = new float[6] { num, num, num, num, num, num };
			}
			if (val.Passive)
			{
				((SL_Character)template).TargetableFactions = (Factions[])(object)new Factions[0];
			}
			else if (combat != null && combat.TargetableFactions != null)
			{
				List<Factions> list = new List<Factions>();
				foreach (string targetableFaction in combat.TargetableFactions)
				{
					if (!TryParseFaction(targetableFaction ?? "", out var f))
					{
						return "CombatSpec.TargetableFactions has unknown faction '" + targetableFaction + "'";
					}
					list.Add(f);
				}
				((SL_Character)template).TargetableFactions = list.ToArray();
			}
			return null;
		}

		private static void StampTreeIcons(NpcSpec spec)
		{
			foreach (SlotDef slot in spec.Trainer.Tree.Slots)
			{
				Item itemPrefab = ResourcesPrefabManager.Instance.GetItemPrefab(slot.SkillId);
				Skill val = (Skill)(object)((itemPrefab is Skill) ? itemPrefab : null);
				if (val != null && (Object)(object)val.SkillTreeIcon == (Object)null && (Object)(object)((Item)val).ItemIcon != (Object)null)
				{
					val.SkillTreeIcon = ((Item)val).ItemIcon;
					Plugin.Log.LogMessage((object)$"[STORYKIT] '{spec.Id}': stamped SkillTreeIcon from ItemIcon for '{((Item)val).Name}' ({slot.SkillId}).");
				}
			}
		}

		private static void OnSpawned(Entry e, Character c, string rpcData)
		{
			e.SpawnCount++;
			e.Live = c;
			RigCharacter(e.Spec, c, $"spawned (#{e.SpawnCount} this session)");
		}

		internal static void RigCharacter(NpcSpec spec, Character c, string origin)
		{
			//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d3: Unknown result type (might be due to invalid IL or missing references)
			ScriptedBodies.Mark(((Component)c).gameObject, "StoryKit", "story NPC '" + spec.Id + "'");
			if (spec.HoldStillInDialogue)
			{
				NpcDialogueHold.Watch(c);
			}
			HealQuestEventRef(c, "'" + spec.Id + "'");
			if (spec.Mobile)
			{
				RigMobile(spec, c, origin);
				return;
			}
			c.NoFall = true;
			SnapToGround(c);
			NpcPin npcPin = ((Component)c).gameObject.GetComponent<NpcPin>();
			if ((Object)(object)npcPin == (Object)null)
			{
				npcPin = ((Component)c).gameObject.AddComponent<NpcPin>();
			}
			if (PhotonNetwork.isNonMasterClientInRoom)
			{
				npcPin.ArmWhenSettled();
			}
			else
			{
				npcPin.Home = ((Component)c).transform.position;
			}
			Plugin.Log.LogMessage((object)$"[STORYKIT] NPC '{spec.Id}' {origin} at {((Component)c).transform.position} in '{SceneManagerHelper.ActiveSceneName}' (NoFall set, pinned).");
			TorsoLookGuard.Watch(c);
			DialogueBuilder.Build(spec, c);
			if ((Object)(object)Plugin.Instance != (Object)null)
			{
				((MonoBehaviour)Plugin.Instance).StartCoroutine(RepairVisualsWatch(spec.Id, c));
			}
		}

		private static void RigMobile(NpcSpec spec, Character c, string origin)
		{
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			Plugin.Log.LogMessage((object)$"[STORYKIT] NPC '{spec.Id}' {origin} at {((Component)c).transform.position} in '{SceneManagerHelper.ActiveSceneName}' (MOBILE: AI-driven, unpinned).");
			SetInvalidAiStates(spec, c);
			TorsoLookGuard.Watch(c);
			MerchantWiring.Attach(spec, c);
			DialogueBuilder.Build(spec, c);
			if ((Object)(object)Plugin.Instance != (Object)null)
			{
				((MonoBehaviour)Plugin.Instance).StartCoroutine(RepairVisualsWatch(spec.Id, c));
			}
		}

		private static void SetInvalidAiStates(NpcSpec spec, Character c)
		{
			CharacterControl characterControl = c.CharacterControl;
			CharacterAI val = (CharacterAI)(object)((characterControl is CharacterAI) ? characterControl : null);
			if ((Object)(object)val == (Object)null)
			{
				val = ((Component)c).GetComponent<CharacterAI>();
			}
			NPCInteraction componentInChildren = ((Component)c).GetComponentInChildren<NPCInteraction>(true);
			if ((Object)(object)val == (Object)null || (Object)(object)componentInChildren == (Object)null || val.AiStates == null)
			{
				Plugin.Log.LogWarning((object)$"[STORYKIT] '{spec.Id}': AI gate not set (CharacterAI={(Object)(object)val != (Object)null} NPCInteraction={(Object)(object)componentInChildren != (Object)null}) — the NPC may be talkable mid-fight.");
				return;
			}
			if (componentInChildren.InvalidAIStates == null)
			{
				componentInChildren.InvalidAIStates = new List<AIState>();
			}
			int num = 0;
			AIState[] aiStates = val.AiStates;
			foreach (AIState val2 in aiStates)
			{
				if (!((Object)(object)val2 == (Object)null) && (val2 is AISCombat || val2 is AISSuspicious) && !componentInChildren.InvalidAIStates.Contains(val2))
				{
					componentInChildren.InvalidAIStates.Add(val2);
					num++;
				}
			}
			Plugin.Log.LogMessage((object)$"[STORYKIT] '{spec.Id}': {num} AI state(s) marked no-talk (combat/suspicious/alert).");
		}

		private static IEnumerator RepairVisualsWatch(string id, Character c)
		{
			float t0 = Time.unscaledTime;
			while (Time.unscaledTime - t0 < 2f)
			{
				if ((Object)(object)c == (Object)null || !Object.op_Implicit((Object)(object)c))
				{
					yield break;
				}
				if ((Object)(object)c.Visuals != (Object)null)
				{
					break;
				}
				yield return null;
			}
			while (Time.unscaledTime - t0 < 3f)
			{
				yield return (object)new WaitForSecondsRealtime(0.25f);
			}
			if ((Object)(object)c == (Object)null || !Object.op_Implicit((Object)(object)c))
			{
				yield break;
			}
			bool flag = Lifecycle.IsGameplayLive();
			int num;
			CharacterVisuals visuals;
			bool flag2;
			Animator val;
			bool flag3;
			CharacterAI val2;
			bool flag4;
			bool flag5;
			try
			{
				num = ((Component)c).gameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true).Length;
				visuals = c.Visuals;
				flag2 = (Object)(object)visuals != (Object)null && visuals.DefaultVisualsInitialized;
				val = c.Animator;
				if ((Object)(object)val == (Object)null)
				{
					val = ((Component)c).gameObject.GetComponent<Animator>();
				}
				if ((Object)(object)val == (Object)null)
				{
					val = ((Component)c).gameObject.GetComponentInChildren<Animator>(true);
				}
				flag3 = (Object)(object)val == (Object)null || !((Behaviour)val).enabled || !val.isInitialized || (Object)(object)val.runtimeAnimatorController == (Object)null;
				CharacterControl characterControl = c.CharacterControl;
				val2 = (CharacterAI)(object)((characterControl is CharacterAI) ? characterControl : null);
				if ((Object)(object)val2 == (Object)null)
				{
					val2 = ((Component)c).GetComponent<CharacterAI>();
				}
				flag4 = (Object)(object)val2 != (Object)null && (Object)(object)val2.AIStatesRoot != (Object)null && !((Component)val2.AIStatesRoot).gameObject.activeSelf;
				flag5 = (Object)(object)val2 != (Object)null && ((CharacterControl)val2).CloseToPlayer;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] posture '" + id + "': census read threw " + ex.GetType().Name + ": " + ex.Message));
				yield break;
			}
			bool flag6 = num == 0 || !flag2;
			string text = $"[STORYKIT] posture '{id}' @{Time.unscaledTime - t0:F1}s: renderers={num} visualsInit={flag2}" + string.Format(" animator={0} init={1} ctrl={2}", (!((Object)(object)val != (Object)null)) ? "NONE" : (((Behaviour)val).enabled ? "on" : "OFF"), (Object)(object)val != (Object)null && val.isInitialized, (Object)(object)val != (Object)null && (Object)(object)val.runtimeAnimatorController != (Object)null) + string.Format(" aiRoot={0} closeToPlayer={1} spawnAnimDone={2} live={3}", ((Object)(object)val2 == (Object)null) ? "n/a" : (flag4 ? "INACTIVE" : "active"), flag5, c.SpawnAnimDone, flag);
			if (!flag6 && !flag3 && !flag4)
			{
				Plugin.Log.LogMessage((object)text);
				yield break;
			}
			List<string> list = new List<string>();
			try
			{
				if (flag6)
				{
					if ((Object)(object)visuals == (Object)null)
					{
						list.Add("bare: no CharacterVisuals holder — cannot repair in place");
					}
					else if (c.VisualData == null)
					{
						list.Add("bare: VisualData null — InitDefaultVisuals would NRE, skipped");
					}
					else
					{
						if (!visuals.DefaultVisualsInitialized)
						{
							visuals.InitDefaultVisuals();
						}
						if ((Object)(object)val != (Object)null)
						{
							val.Rebind();
						}
						int num2 = ((Component)c).gameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true).Length;
						list.Add($"bare: InitDefaultVisuals+Rebind, renderers {num}->{num2}" + ((num2 == 0) ? " STILL BARE" : ""));
					}
				}
				else if (flag3 && (Object)(object)val != (Object)null)
				{
					((Behaviour)val).enabled = true;
					val.Rebind();
					list.Add($"animator unbound: enabled+Rebind (init now {val.isInitialized})");
				}
				else if (flag3)
				{
					list.Add("animator: NONE on the body — nothing to rebind");
				}
				if (flag4)
				{
					if (flag && ((Behaviour)val2).isActiveAndEnabled && c.SpawnAnimDone && c.AliveUndying && !AiRootDisabledByQuest(val2) && !AiQuestCheckPending(val2))
					{
						((Component)val2.AIStatesRoot).gameObject.SetActive(true);
						list.Add("aiRoot inactive on a live world: re-activated (vanilla's own rule held)");
					}
					else
					{
						list.Add("aiRoot inactive: left alone (" + ((!flag) ? "world not live yet" : ((!((Behaviour)val2).isActiveAndEnabled) ? "CharacterAI disabled (out of DistanceToEnable?)" : ((!c.SpawnAnimDone) ? "spawn anim pending" : ((!c.AliveUndying) ? "not alive" : (AiQuestCheckPending(val2) ? "quest-event check pending" : "AI root disabled by quest gate"))))) + ")");
					}
				}
			}
			catch (Exception ex2)
			{
				list.Add("repair threw " + ex2.GetType().Name + ": " + ex2.Message);
			}
			Plugin.Log.LogWarning((object)(text + " — " + string.Join("; ", list.ToArray()) + (flag6 ? (". If still bare: 'storynpcdespawn " + id + "' then 'storynpcspawn " + id + "'.") : ".")));
		}

		private static bool AiRootDisabledByQuest(CharacterAI ai)
		{
			try
			{
				return s_disableAIRoot != null && (Object)(object)ai != (Object)null && (bool)s_disableAIRoot.GetValue(ai);
			}
			catch
			{
				return false;
			}
		}

		private static bool AiQuestCheckPending(CharacterAI ai)
		{
			try
			{
				return s_checkQuestEvent != null && (Object)(object)ai != (Object)null && (bool)s_checkQuestEvent.GetValue(ai);
			}
			catch
			{
				return false;
			}
		}

		private static bool HealQuestEventRef(CharacterAI ai)
		{
			try
			{
				if ((Object)(object)ai == (Object)null || s_aiActiveOnQuestEvent == null)
				{
					return false;
				}
				if (s_aiActiveOnQuestEvent.GetValue(ai) != null)
				{
					return false;
				}
				s_aiActiveOnQuestEvent.SetValue(ai, Activator.CreateInstance(s_aiActiveOnQuestEvent.FieldType));
				return true;
			}
			catch
			{
				return false;
			}
		}

		private static void HealQuestEventRef(Character c, string who)
		{
			if (!((Object)(object)c == (Object)null) && Object.op_Implicit((Object)(object)c))
			{
				CharacterControl characterControl = c.CharacterControl;
				CharacterAI val = (CharacterAI)(object)((characterControl is CharacterAI) ? characterControl : null);
				if ((Object)(object)val == (Object)null)
				{
					val = ((Component)c).GetComponent<CharacterAI>();
				}
				if (!((Object)(object)val == (Object)null) && HealQuestEventRef(val))
				{
					Plugin.Log.LogInfo((object)("[STORYKIT] " + who + ": CharacterAI.m_aiActiveOnQuestEvent was null — filled with an empty QuestEventReference (SK-D1-NRE: vanilla's OnEnable/OnDisable dereference it unguarded)."));
				}
			}
		}

		private static void SnapToGround(Character c)
		{
			//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_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_0046: 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)
			//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)
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0087: Unknown result type (might be due to invalid IL or missing references)
			//IL_008d: Unknown result type (might be due to invalid IL or missing references)
			Vector3 position = ((Component)c).transform.position;
			NavMeshHit val = default(NavMeshHit);
			RaycastHit val2 = default(RaycastHit);
			if (NavMesh.SamplePosition(position, ref val, 6f, -1))
			{
				((Component)c).transform.position = new Vector3(position.x, ((NavMeshHit)(ref val)).position.y, position.z);
			}
			else if (Physics.Raycast(position + Vector3.up * 1.2f, Vector3.down, ref val2, 30f, -1, (QueryTriggerInteraction)1))
			{
				((Component)c).transform.position = new Vector3(position.x, ((RaycastHit)(ref val2)).point.y, position.z);
			}
			else
			{
				Plugin.Log.LogWarning((object)$"[STORYKIT] ground-snap found no navmesh or geometry under {position} — leaving Y as placed (may float; re-stamp on solid ground).");
			}
		}

		public static bool TrySpawn(string id)
		{
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			Entry entry = Get(id);
			if (entry == null)
			{
				return false;
			}
			if (entry.Spec.Placements == null || entry.Spec.Placements.Count == 0)
			{
				Plugin.Log.LogWarning((object)("[STORYKIT] '" + id + "': no placement to spawn at."));
				return false;
			}
			Placement val = entry.Spec.Placements[0];
			return SpawnAt(id, new Vector3(val.X, val.Y, val.Z), val.RotationY);
		}

		public static bool SpawnAt(string id, Vector3 pos, float rotY)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			return SpawnAt(id, pos, rotY, SpawnPolicy.Defer);
		}

		public static bool SpawnAt(string id, Vector3 pos, float rotY, SpawnPolicy policy)
		{
			//IL_0001: Unknown result type (might be