Decompiled source of StoryKit v0.1.12
plugins/StoryKit.Core.dll
Decompiled 2 weeks agousing 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
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.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