Decompiled source of SpawnKit v0.5.2
plugins/SpawnKit.Core.dll
Decompiled 6 days agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using Microsoft.CodeAnalysis; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] [assembly: AssemblyCompany("SpawnKit.Core")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.5.2.0")] [assembly: AssemblyInformationalVersion("0.5.2+091b206910305beb491301afe1db01b8cd7b8e72")] [assembly: AssemblyProduct("SpawnKit.Core")] [assembly: AssemblyTitle("SpawnKit.Core")] [assembly: AssemblyMetadata("BuildStamp", "091b2069 2026-08-28")] [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 SpawnKit.Core { public enum AiLiveness { NoAiGraph, WorldPaused, AiBehaviourDisabled, NotStartInitialized, DistanceCulled, Ticking } public static class AiLivenessRules { public static AiLiveness Classify(bool hasAi, int aiStateCount, bool aiBehaviourEnabled, bool startInitDone, bool closeToPlayer, bool gameplayPaused) { if (!hasAi || aiStateCount <= 0) { return AiLiveness.NoAiGraph; } if (gameplayPaused) { return AiLiveness.WorldPaused; } if (!aiBehaviourEnabled) { return AiLiveness.AiBehaviourDisabled; } if (!startInitDone) { return AiLiveness.NotStartInitialized; } if (!closeToPlayer) { return AiLiveness.DistanceCulled; } return AiLiveness.Ticking; } public static bool CanEngageUnprompted(AiLiveness v) { return v == AiLiveness.Ticking; } public static string Explain(AiLiveness v) { return v switch { AiLiveness.NoAiGraph => "no CharacterAI or an empty AI-state graph — this body can never fight; TemplateAiGate should have refused it upstream (report it).", AiLiveness.WorldPaused => "the WORLD SIM is paused (NetworkLevelLoader.IsGameplayPaused) — no creature, spawned OR vanilla, detects/wanders/culls while this holds, yet every dev verb still answers. Run Beastwhispering's 'unstick fix' and re-check before trusting any real-time combat result.", AiLiveness.AiBehaviourDisabled => "the CharacterAI Behaviour is disabled — usually CharAIDisable's VeryFar leg; get a player within DistanceToEnable (or raise [AI] AiDisableDistance).", AiLiveness.NotStartInitialized => "Character.IsStartInitDone is false — ProcessInit never completed, so detection returns at its first guard forever even though wander still runs. Suspect the visual/rig init chain ('ghostdiag <name> fix').", AiLiveness.DistanceCulled => "distance-culled by CharAIDisable (no player inside DistanceToEnable) — close the gap.", AiLiveness.Ticking => "live — this body can detect and engage on its own.", _ => "unknown.", }; } public static string Format(AiLiveness v) { if (v != AiLiveness.Ticking) { return v.ToString() + " (" + Explain(v) + ")"; } return "Ticking"; } } public static class CasterSkillGate { public const string CensusError = "skills=?/?"; public static List<int> MissingSkillIds(IEnumerable<int> learnedIds, IEnumerable<int> childSkillIds) { List<int> list = new List<int>(); if (childSkillIds == null) { return list; } HashSet<int> hashSet = new HashSet<int>(); if (learnedIds != null) { foreach (int learnedId in learnedIds) { hashSet.Add(learnedId); } } HashSet<int> hashSet2 = new HashSet<int>(); foreach (int childSkillId in childSkillIds) { if (!hashSet.Contains(childSkillId) && hashSet2.Add(childSkillId)) { list.Add(childSkillId); } } return list; } public static string Census(int learnedCount, int childCount, bool hasKnowledge) { if (!hasKnowledge) { return "skills=-/-"; } return $"skills={learnedCount}/{childCount}"; } } public enum CorpseLootVerdict { Alive, NoLootableComponent, ComponentDisabled, NoPouch, NoPouchInteractionTrigger, DropsPresentButInert, NoDropsConfigured, Lootable } public readonly struct CorpseLootObservation { public readonly bool IsDead; public readonly bool HasComponent; public readonly bool ComponentEnabled; public readonly bool HasPouch; public readonly bool PouchHasInteractionTrigger; public readonly int LootDropEntries; public readonly int LootDropsWithDropper; public readonly int SkinDropEntries; public readonly int SkinDropsWithDropper; public readonly bool Lootable; public readonly bool Skinable; public CorpseLootObservation(bool isDead, bool hasComponent, bool componentEnabled, bool hasPouch, bool pouchHasInteractionTrigger, int lootDropEntries, int lootDropsWithDropper, int skinDropEntries, int skinDropsWithDropper, bool lootable, bool skinable) { IsDead = isDead; HasComponent = hasComponent; ComponentEnabled = componentEnabled; HasPouch = hasPouch; PouchHasInteractionTrigger = pouchHasInteractionTrigger; LootDropEntries = lootDropEntries; LootDropsWithDropper = lootDropsWithDropper; SkinDropEntries = skinDropEntries; SkinDropsWithDropper = skinDropsWithDropper; Lootable = lootable; Skinable = skinable; } } public static class CorpseLootDiagnosis { public static CorpseLootVerdict Classify(in CorpseLootObservation o) { if (!o.IsDead) { return CorpseLootVerdict.Alive; } if (!o.HasComponent) { return CorpseLootVerdict.NoLootableComponent; } if (!o.ComponentEnabled) { return CorpseLootVerdict.ComponentDisabled; } if (!o.HasPouch) { return CorpseLootVerdict.NoPouch; } if (!o.PouchHasInteractionTrigger) { return CorpseLootVerdict.NoPouchInteractionTrigger; } if (o.Lootable || o.Skinable) { return CorpseLootVerdict.Lootable; } if (o.LootDropsWithDropper > 0 || o.SkinDropsWithDropper > 0) { return CorpseLootVerdict.DropsPresentButInert; } return CorpseLootVerdict.NoDropsConfigured; } public static bool IsActionableDefect(CorpseLootVerdict v) { if ((uint)(v - 2) <= 3u) { return true; } return false; } public static string Explain(CorpseLootVerdict v) { return v switch { CorpseLootVerdict.Alive => "not dead yet — probe again after death", CorpseLootVerdict.NoLootableComponent => "no LootableOnDeath component — this body never had corpse loot", CorpseLootVerdict.ComponentDisabled => "LootableOnDeath is DISABLED -> OnDeath early-returns (ForceLootableEnabled re-enables it at mint)", CorpseLootVerdict.NoPouch => "Inventory.Pouch is null -> the corpse cannot be made lootable", CorpseLootVerdict.NoPouchInteractionTrigger => "pouch has no interaction trigger -> OnDeath early-returns before MakeLootable", CorpseLootVerdict.DropsPresentButInert => "drop entries carry droppers yet m_lootable/m_skinable are false -> LootableOnDeath.Start did not populate (BUG)", CorpseLootVerdict.NoDropsConfigured => "no loot AND no skin droppers -> empty undetectable pouch; this creature has no corpse loot by design (scripted/boss reward), NOT a SpawnKit regression", CorpseLootVerdict.Lootable => "m_lootable/m_skinable set -> a normal loot/skin prompt is expected", _ => "unknown", }; } } public enum CorpsePolicy { Vanilla, NoBody } public static class CorpseRules { public static CorpsePolicy EffectivePolicy(CorpsePolicy? requested, CorpsePolicy configured) { return requested ?? configured; } public static float EffectiveLinger(float? requested, float configured) { float num = requested ?? configured; if (!(num < 0f)) { return num; } return 0f; } } public enum RemovalReason { SilentDespawn, Killed, WatchDied, WatchDespawned, CancelledPending } public static class DisengagePolicy { public static bool NeedsDisengage(RemovalReason reason) { return reason != RemovalReason.CancelledPending; } public static bool ByReference(bool exists) { return exists; } public static bool IsStaleEntry(bool entryExists, bool entryAlive) { if (entryExists) { return !entryAlive; } return true; } } public static class ExpeditionRow { public struct Buttons { public bool ShowPrewarm; public bool ShowWarmTrip; public bool Interactive; public string SpawnLabel; public bool SpawnCostsATrip; } public struct Arm { public string Key; public float At; } public struct ClickResult { public bool Fire; public Arm State; } public static Buttons For(bool bodyReady, bool expeditionOnly, bool expeditionsAllowed, bool tripInFlight, bool armed) { Buttons result = new Buttons { Interactive = !tripInFlight, SpawnLabel = "Spawn" }; if (tripInFlight) { return result; } bool flag = expeditionOnly && !bodyReady && expeditionsAllowed; result.ShowPrewarm = !bodyReady && !expeditionOnly; result.ShowWarmTrip = flag; result.SpawnCostsATrip = flag; if (flag) { result.SpawnLabel = (armed ? "Confirm?" : "Spawn (trip)"); } return result; } public static ClickResult Click(Arm current, string key, float now, float windowSeconds, bool confirmRequired) { if (!confirmRequired) { return new ClickResult { Fire = true, State = default(Arm) }; } if (IsArmed(current, key, now, windowSeconds)) { return new ClickResult { Fire = true, State = default(Arm) }; } return new ClickResult { Fire = false, State = new Arm { Key = key, At = now } }; } public static bool IsArmed(Arm current, string key, float now, float windowSeconds) { if (!string.IsNullOrEmpty(current.Key) && string.Equals(current.Key, key, StringComparison.OrdinalIgnoreCase) && now - current.At >= 0f) { return now - current.At <= windowSeconds; } return false; } } public enum FailReason { None, NotInitialized, Disabled, NotMaster, NoPlayer, NoAIManager, CapExceeded, UnknownSpecies, MintFailed, Cancelled, HarvestFailed, RoomWithGuests, PeersNotReady, SpeciesColdOnPeer } public struct ForgivenEntry { public int Actor; public string Uid; public string Species; public double At; } public sealed class ForgivenLedger { public const int MaxEntries = 64; private readonly List<ForgivenEntry> _entries = new List<ForgivenEntry>(); public int Count => _entries.Count; private static string Norm(string? s) { if (s != null) { return s.Trim(); } return ""; } private int IndexOf(int actor, string normUid) { for (int i = 0; i < _entries.Count; i++) { if (_entries[i].Actor == actor && string.Equals(Norm(_entries[i].Uid), normUid, StringComparison.Ordinal)) { return i; } } return -1; } public bool Add(int actor, string? uid, string? species, double now) { string text = Norm(uid); if (text.Length == 0) { return false; } if (IndexOf(actor, text) >= 0) { return true; } _entries.Add(new ForgivenEntry { Actor = actor, Uid = text, Species = Norm(species), At = now }); while (_entries.Count > 64) { _entries.RemoveAt(0); } return true; } public List<ForgivenEntry> MatchWarm(int actor, string[]? warm) { List<ForgivenEntry> list = new List<ForgivenEntry>(); for (int num = _entries.Count - 1; num >= 0; num--) { if (_entries[num].Actor == actor && SpeciesRoomPolicy.Contains(warm, _entries[num].Species)) { list.Add(_entries[num]); _entries.RemoveAt(num); } } list.Reverse(); return list; } public List<ForgivenEntry> TakeExpired(double now, double lifetimeSeconds) { List<ForgivenEntry> list = new List<ForgivenEntry>(); if (lifetimeSeconds <= 0.0) { return list; } for (int num = _entries.Count - 1; num >= 0; num--) { if (!(now - _entries[num].At < lifetimeSeconds)) { list.Add(_entries[num]); _entries.RemoveAt(num); } } list.Reverse(); return list; } public void ForgetUid(string? uid) { string text = Norm(uid); if (text.Length == 0) { return; } for (int num = _entries.Count - 1; num >= 0; num--) { if (string.Equals(Norm(_entries[num].Uid), text, StringComparison.Ordinal)) { _entries.RemoveAt(num); } } } public void ForgetActor(int actor) { for (int num = _entries.Count - 1; num >= 0; num--) { if (_entries[num].Actor == actor) { _entries.RemoveAt(num); } } } public void Clear() { _entries.Clear(); } public List<ForgivenEntry> Snapshot() { return new List<ForgivenEntry>(_entries); } } public enum WantOutcome { Queued, AlreadyQueued, DeadEnd, Empty } public sealed class GuestHarvestStanding { private struct Entry { public string Key; public WantClass Class; } private static readonly StringComparer KeyComparer = StringComparer.OrdinalIgnoreCase; private readonly List<Entry> _wanted = new List<Entry>(); private readonly List<string> _deadEnds = new List<string>(); public int CyclesSpent { get; private set; } public int VoluntaryCyclesSpent { get; private set; } public bool Warming { get; set; } public int Generation { get; private set; } public int WantedCount => _wanted.Count; private static string Norm(string key) { if (key != null) { return key.Trim(); } return ""; } private static bool Holds(List<string> keys, string normKey) { for (int i = 0; i < keys.Count; i++) { if (KeyComparer.Equals(Norm(keys[i]), normKey)) { return true; } } return false; } private int IndexOfWanted(string normKey) { for (int i = 0; i < _wanted.Count; i++) { if (KeyComparer.Equals(_wanted[i].Key, normKey)) { return i; } } return -1; } public bool IsCurrent(int generation) { return generation == Generation; } public bool ReleaseWarm(int generation) { if (!IsCurrent(generation)) { return false; } Warming = false; return true; } public List<string> Wanted() { List<string> list = new List<string>(_wanted.Count); for (int i = 0; i < _wanted.Count; i++) { list.Add(_wanted[i].Key); } return list; } public List<KeyValuePair<string, WantClass>> WantedWithClass() { List<KeyValuePair<string, WantClass>> list = new List<KeyValuePair<string, WantClass>>(_wanted.Count); for (int i = 0; i < _wanted.Count; i++) { list.Add(new KeyValuePair<string, WantClass>(_wanted[i].Key, _wanted[i].Class)); } return list; } public bool TryGetClass(string species, out WantClass cls) { int num = IndexOfWanted(Norm(species)); cls = ((num >= 0) ? _wanted[num].Class : WantClass.HostWant); return num >= 0; } public List<string> DeadEnds() { return new List<string>(_deadEnds); } public WantOutcome Want(string species, bool front) { return Want(species, front, WantClass.HostWant); } public WantOutcome Want(string species, bool front, WantClass cls) { string text = Norm(species); if (text.Length == 0) { return WantOutcome.Empty; } if (Holds(_deadEnds, text)) { return WantOutcome.DeadEnd; } if (IndexOfWanted(text) >= 0) { return WantOutcome.AlreadyQueued; } Entry item = new Entry { Key = text, Class = cls }; if (front) { _wanted.Insert(0, item); } else { _wanted.Add(item); } return WantOutcome.Queued; } public bool TryTakeNext(out string species) { WantClass cls; return TryTakeNext(out species, out cls); } public bool TryTakeNext(out string species, out WantClass cls) { if (!TryPeekNext(out species, out cls)) { return false; } _wanted.RemoveAt(0); return true; } public bool TryPeekAt(int index, out string species, out WantClass cls) { if (index < 0 || index >= _wanted.Count) { species = null; cls = WantClass.HostWant; return false; } species = _wanted[index].Key; cls = _wanted[index].Class; return true; } public bool RemoveAt(int index) { if (index < 0 || index >= _wanted.Count) { return false; } _wanted.RemoveAt(index); return true; } public bool TryPeekNext(out string species, out WantClass cls) { if (_wanted.Count == 0) { species = null; cls = WantClass.HostWant; return false; } species = _wanted[0].Key; cls = _wanted[0].Class; return true; } public void MarkDeadEnd(string species) { string text = Norm(species); if (text.Length != 0 && !Holds(_deadEnds, text)) { _deadEnds.Add(text); } } public void ChargeCycles(int delta) { ChargeCycles(delta, WantClass.HostWant); } public void ChargeCycles(int delta, WantClass cls) { if (delta > 0) { if (cls == WantClass.Voluntary) { VoluntaryCyclesSpent += delta; } else { CyclesSpent += delta; } } } public void ClearWanted() { _wanted.Clear(); } public int ClearWanted(WantClass cls) { int num = 0; for (int num2 = _wanted.Count - 1; num2 >= 0; num2--) { if (_wanted[num2].Class == cls) { _wanted.RemoveAt(num2); num++; } } return num; } public void Reset() { _wanted.Clear(); _deadEnds.Clear(); CyclesSpent = 0; VoluntaryCyclesSpent = 0; Warming = false; Generation++; } } public enum WantClass { HostWant, Voluntary } public enum GuestWarmVerdict { Warm, RefuseBudget, RefuseCeiling, RefuseAdmission, RefuseCombat, RefuseDisabled, NotGuest } public static class GuestWarmPolicy { public static bool Unlimited(int budget) { return budget <= 0; } public static GuestWarmVerdict Decide(WantClass cls, int cyclesSpent, int mirrorBudget, int voluntarySpent, int voluntaryBudget, int sessionCycles, int sessionCeiling, bool admissionOk, bool inCombat, bool guestPrewarmEnabled, bool isGuest) { if (!isGuest) { return GuestWarmVerdict.NotGuest; } if (cls == WantClass.Voluntary && !guestPrewarmEnabled) { return GuestWarmVerdict.RefuseDisabled; } if (!Unlimited(sessionCeiling) && sessionCycles >= sessionCeiling) { return GuestWarmVerdict.RefuseCeiling; } if (cls == WantClass.Voluntary) { if (!Unlimited(voluntaryBudget) && voluntarySpent >= voluntaryBudget) { return GuestWarmVerdict.RefuseBudget; } } else if (!Unlimited(mirrorBudget) && cyclesSpent >= mirrorBudget) { return GuestWarmVerdict.RefuseBudget; } if (!admissionOk) { return GuestWarmVerdict.RefuseAdmission; } if (inCombat) { return GuestWarmVerdict.RefuseCombat; } return GuestWarmVerdict.Warm; } public static string Reason(GuestWarmVerdict v, WantClass cls) { switch (v) { case GuestWarmVerdict.Warm: return ""; case GuestWarmVerdict.RefuseBudget: if (cls != WantClass.Voluntary) { return "mirror-budget"; } return "voluntary-budget"; case GuestWarmVerdict.RefuseCeiling: return "session-ceiling"; case GuestWarmVerdict.RefuseAdmission: return "admission"; case GuestWarmVerdict.RefuseCombat: return "in-combat"; case GuestWarmVerdict.RefuseDisabled: return "disabled"; case GuestWarmVerdict.NotGuest: return "not-guest"; default: return v.ToString(); } } public static bool IsTransient(GuestWarmVerdict v) { if (v != GuestWarmVerdict.RefuseAdmission) { return v == GuestWarmVerdict.RefuseCombat; } return true; } } public enum CursorStrategy { None, GameSeamReleased, VanillaMenuOwns } public static class MenuCursorPolicy { public static bool EngageSeam(bool menuOpen, bool vanillaMenuFocused) { if (menuOpen) { return !vanillaMenuFocused; } return false; } public static bool ForceClose(bool menuOpen, bool vanillaMenuFocused) { return menuOpen && vanillaMenuFocused; } public static bool CanOpen(bool vanillaMenuFocused) { return !vanillaMenuFocused; } public static bool CloseOnEsc(bool menuOpen, bool escPressed) { return menuOpen && escPressed; } public static CursorStrategy Resolve(bool menuOpen, bool vanillaMenuFocused) { if (!menuOpen) { return CursorStrategy.None; } if (!vanillaMenuFocused) { return CursorStrategy.GameSeamReleased; } return CursorStrategy.VanillaMenuOwns; } public static CursorStrategy ResolveLog(bool nowOpen, bool vanillaMenuFocused) { if (vanillaMenuFocused) { return CursorStrategy.VanillaMenuOwns; } if (!nowOpen) { return CursorStrategy.None; } return CursorStrategy.GameSeamReleased; } } public static class MintHeal { public struct SettleState { public float PrevMax; public float StartMax; public float StartHp; public int StableTicks; public int Ticks; public float Elapsed; public bool SawChange; public bool IsStable => StableTicks >= 3; public static SettleState Start(float prevMax) { return new SettleState { PrevMax = prevMax, StartMax = prevMax, StartHp = -1f }; } } public const float SettleBudget = 1f; public const float Tick = 0.1f; public const int StableTicks = 3; public static float Initial(float cur, float max) { if (!(max > 0f)) { return -1f; } if (cur >= max) { return -1f; } return max; } public static float Next(float prevMax, float cur, float max) { if (!(max > 0f)) { return -1f; } if (max == prevMax) { return -1f; } float num = cur + (max - prevMax); if (num > max) { num = max; } if (num < 0f) { num = 0f; } return num; } public static bool Stable(float prevMax, float max) { return max == prevMax; } public static bool Done(int stableTicks, float elapsed, bool sawChange) { if (!(elapsed >= 1f)) { if (sawChange) { return stableTicks >= 3; } return false; } return true; } public static float Step(ref SettleState s, float cur, float max) { s.Elapsed += 0.1f; s.Ticks++; if (s.StartHp < 0f) { s.StartHp = cur; } if (Stable(s.PrevMax, max)) { s.StableTicks++; } else { s.StableTicks = 0; s.SawChange = true; } float result = Next(s.PrevMax, cur, max); s.PrevMax = max; return result; } public static bool Done(in SettleState s) { return Done(s.StableTicks, s.Elapsed, s.SawChange); } } public enum PublishPayload { ReusePollBuffer, EncodeFresh } public static class MirrorEncodeReuse { public static PublishPayload Choose(bool hasBuffer, double bufferedAt, double now) { if (!hasBuffer || bufferedAt != now) { return PublishPayload.EncodeFresh; } return PublishPayload.ReusePollBuffer; } } public static class MirrorFailPolicy { public static bool IsBenign(string? reason) { return string.Equals(reason, "not-ready", StringComparison.Ordinal); } public static bool ShouldDespawnColdUnsafe(bool roomGateOk, int priorColdUnsafeCount) { if (roomGateOk) { return priorColdUnsafeCount >= 1; } return true; } } public sealed class MirrorInFlight { private readonly HashSet<string> _inFlight = new HashSet<string>(StringComparer.Ordinal); private readonly Dictionary<string, SpawnNetProtocol.GoneKind> _tombstones = new Dictionary<string, SpawnNetProtocol.GoneKind>(StringComparer.Ordinal); public int InFlightCount => _inFlight.Count; public int TombstoneCount => _tombstones.Count; public void Begin(string uid) { if (!string.IsNullOrEmpty(uid)) { _inFlight.Add(uid); } } public void Finish(string uid) { if (!string.IsNullOrEmpty(uid)) { _inFlight.Remove(uid); _tombstones.Remove(uid); } } public bool IsInFlight(string uid) { if (!string.IsNullOrEmpty(uid)) { return _inFlight.Contains(uid); } return false; } public bool RecordGone(string uid, SpawnNetProtocol.GoneKind kind) { if (!IsInFlight(uid)) { return false; } if (_tombstones.TryGetValue(uid, out var value) && Rank(value) >= Rank(kind)) { return true; } _tombstones[uid] = kind; return true; } public bool TryPeek(string uid, out SpawnNetProtocol.GoneKind kind) { if (!string.IsNullOrEmpty(uid) && _tombstones.TryGetValue(uid, out kind)) { return true; } kind = SpawnNetProtocol.GoneKind.Despawned; return false; } public void Clear() { _inFlight.Clear(); _tombstones.Clear(); } private static int Rank(SpawnNetProtocol.GoneKind kind) { if (kind != SpawnNetProtocol.GoneKind.Died) { return 2; } return 1; } } public static class ReplicaShape { public enum ViewSyncMode { Unknown, Off, Unreliable, Other } public struct ShapeCensus { public bool HasCharacterAI; public int NccCount; public bool NccEnabled; public bool CloseToPlayer; public bool HasCharAIDisable; public bool AgentPresent; public bool AgentEnabled; public bool AgentUpdatePosition; public bool AgentUpdateRotation; public int ActiveAiRoots; public bool CcPresent; public bool CcEnabled; public ViewSyncMode SyncMode; public bool IsAI; } [Flags] public enum ShapeFix { None = 0, KillStaleNcc = 1, AddFreshNcc = 2, EnableNcc = 4, SetCloseToPlayer = 8, DisableAgent = 0x10, DeactivateAiRoots = 0x20, EnableCc = 0x40, KillCharacterAI = 0x80, ForceUnreliableSync = 0x100 } public enum ShapeVerdict { Healthy, Corrected, RebuiltDrive } public struct ShapePlan { public ShapeVerdict Verdict; public ShapeFix Fixes; public bool AiGateMissing; } public struct GateEval { public bool OuterGate; public bool FullGate; public bool SyncOk; public bool ConvergedOk; } public enum InitNudge { None, EnableAndInitWanted, DirectEquipFlag } public const int InitNudgeEscalateAttempts = 3; public static ShapePlan DecideTemplateClone(ShapeCensus c) { if (c.HasCharacterAI) { if (c.NccCount == 0) { return new ShapePlan { Verdict = ShapeVerdict.Healthy, Fixes = ShapeFix.None }; } return new ShapePlan { Verdict = ShapeVerdict.Corrected, Fixes = ShapeFix.KillStaleNcc }; } ShapeFix shapeFix = ShapeFix.AddFreshNcc | ShapeFix.SetCloseToPlayer; if (c.NccCount > 0) { shapeFix |= ShapeFix.KillStaleNcc; } if (c.AgentPresent && (c.AgentEnabled || c.AgentUpdatePosition || c.AgentUpdateRotation)) { shapeFix |= ShapeFix.DisableAgent; } if (c.ActiveAiRoots > 0) { shapeFix |= ShapeFix.DeactivateAiRoots; } if (c.CcPresent && !c.CcEnabled) { shapeFix |= ShapeFix.EnableCc; } return new ShapePlan { Verdict = ShapeVerdict.RebuiltDrive, Fixes = shapeFix }; } public static ShapePlan DecideActiveReplica(ShapeCensus c) { ShapeFix shapeFix = ShapeFix.None; bool flag = false; if (c.HasCharacterAI) { shapeFix |= ShapeFix.KillCharacterAI; flag = true; } if (c.NccCount == 0) { shapeFix |= ShapeFix.AddFreshNcc | ShapeFix.SetCloseToPlayer; flag = true; } else { if (c.NccCount > 1) { shapeFix |= ShapeFix.KillStaleNcc; flag = true; } if (!c.NccEnabled) { shapeFix |= ShapeFix.EnableNcc; } if (!c.CloseToPlayer && !c.HasCharAIDisable) { shapeFix |= ShapeFix.SetCloseToPlayer; } } if (c.AgentPresent && (c.AgentEnabled || c.AgentUpdatePosition || c.AgentUpdateRotation)) { shapeFix |= ShapeFix.DisableAgent; } if (c.ActiveAiRoots > 0) { shapeFix |= ShapeFix.DeactivateAiRoots; } if (c.CcPresent && !c.CcEnabled) { shapeFix |= ShapeFix.EnableCc; } if (c.SyncMode == ViewSyncMode.Off) { shapeFix |= ShapeFix.ForceUnreliableSync; } ShapeVerdict verdict = ((shapeFix != ShapeFix.None) ? ((!flag) ? ShapeVerdict.Corrected : ShapeVerdict.RebuiltDrive) : ShapeVerdict.Healthy); return new ShapePlan { Verdict = verdict, Fixes = shapeFix, AiGateMissing = !c.IsAI }; } public static GateEval EvalGates(bool isAI, bool sendInit, bool loadDone, bool init, bool closeToPlayer, bool ccEnabled, ViewSyncMode sync) { bool flag = isAI || sendInit || !loadDone; bool flag2 = init && loadDone && closeToPlayer && ccEnabled; bool flag3 = sync == ViewSyncMode.Unreliable; return new GateEval { OuterGate = flag, FullGate = flag2, SyncOk = flag3, ConvergedOk = (flag && flag2 && flag3) }; } public static InitNudge DecideInitNudge(bool initialized, bool equipInit, bool hasStartingEquipment, int attempts) { if (initialized || equipInit) { return InitNudge.None; } if (!hasStartingEquipment) { return InitNudge.DirectEquipFlag; } if (attempts < 3) { return InitNudge.EnableAndInitWanted; } return InitNudge.DirectEquipFlag; } public static string Describe(ShapeCensus c) { return string.Format("charAI={0} ncc={1}(en={2} ctp={3}) ", c.HasCharacterAI ? "T" : "F", c.NccCount, c.NccEnabled ? "T" : "F", c.CloseToPlayer ? "T" : "F") + "caid=" + (c.HasCharAIDisable ? "T" : "F") + " agent=" + ((!c.AgentPresent) ? "none" : (c.AgentEnabled ? "ON" : "off")) + "/updPos=" + (c.AgentUpdatePosition ? "T" : "F") + "/updRot=" + (c.AgentUpdateRotation ? "T" : "F") + " " + string.Format("aiRootsActive={0} cc={1}", c.ActiveAiRoots, (!c.CcPresent) ? "none" : (c.CcEnabled ? "on" : "OFF")); } } public static class RigGate { public struct RigCensus { public int LiveHitboxes; public int CapturedHitboxes; public bool HasRagdollRoot; public bool RagdollIsHitbox; public int RagdollHitboxColliders; public int AttackTransforms; public int InactiveRigObjects; public bool LockingPointAsleep; public int RagdollRigidbodyCache; public int RagdollJointManagers; public int RagdollManagersWithJoint; public bool StartInitMissing; } public static bool NeedsReinit(in RigCensus c, out string reason) { if (c.StartInitMissing) { reason = "vanilla init never ran (Character.Start skipped)"; return true; } if (c.LiveHitboxes > c.CapturedHitboxes) { reason = $"hitboxes uncollected ({c.CapturedHitboxes}/{c.LiveHitboxes} captured)"; return true; } if (c.LiveHitboxes > 0 && c.CapturedHitboxes <= 0) { reason = "no hitboxes captured"; return true; } if (c.HasRagdollRoot && c.RagdollIsHitbox && c.RagdollHitboxColliders <= 0) { reason = "ragdoll hitbox colliders empty"; return true; } if (c.AttackTransforms == 0) { reason = "no attack transforms"; return true; } if (c.InactiveRigObjects > 0) { reason = $"{c.InactiveRigObjects} inactive rig object(s)"; return true; } if (c.LockingPointAsleep) { reason = "locking point asleep"; return true; } reason = "healthy"; return false; } public static bool NeedsRagdollInit(in RigCensus c, out string reason) { if (!c.HasRagdollRoot) { reason = "no ragdoll root"; return false; } if (c.RagdollRigidbodyCache <= 0 && c.RagdollJointManagers <= 0) { reason = "ragdoll caches empty"; return true; } if (c.RagdollJointManagers > 0 && c.RagdollManagersWithJoint <= 0) { reason = $"ragdoll already built but jointless ({c.RagdollJointManagers} managers) — NOT repairable"; return false; } reason = $"ragdoll already built ({c.RagdollJointManagers} managers, {c.RagdollManagersWithJoint} jointed)"; return false; } } public static class RingPlacement { public const float FarRingFactor = 1.6f; public const float StepDegrees = 45f; public static IReadOnlyList<(float x, float z)> Candidates(float forwardX, float forwardZ, float distance, int count) { if (count <= 0) { return Array.Empty<(float, float)>(); } if (distance <= 0f) { throw new ArgumentOutOfRangeException("distance"); } float num = (float)Math.Sqrt(forwardX * forwardX + forwardZ * forwardZ); float num2; float num3; if (num < 0.0001f) { num2 = 0f; num3 = 1f; } else { num2 = forwardX / num; num3 = forwardZ / num; } List<(float, float)> list = new List<(float, float)>(count); int num4 = (int)Math.Round(8.0); for (int i = 0; i < count; i++) { int num5 = i % num4; float num6 = ((i < num4) ? distance : (distance * 1.6f)); double num7 = (double)((num5 == 0) ? 0f : ((num5 == num4 - 1) ? 180f : (45f * (float)((num5 + 1) / 2) * ((num5 % 2 == 1) ? 1f : (-1f))))) * Math.PI / 180.0; float num8 = (float)Math.Cos(num7); float num9 = (float)Math.Sin(num7); float num10 = num2 * num8 + num3 * num9; float num11 = (0f - num2) * num9 + num3 * num8; list.Add((num10 * num6, num11 * num6)); } return list; } } public struct WarmRequest { public int Actor; public string Species; } public static class RoomWarmRequest { private static readonly StringComparer KeyComparer = StringComparer.OrdinalIgnoreCase; private static string Norm(string? key) { if (key != null) { return key.Trim(); } return ""; } public static List<WarmRequest> Plan(IReadOnlyList<string>? prioritySpecies, IReadOnlyList<PeerWarmRow>? peers, int perPeerCap) { return Plan(prioritySpecies, peers, perPeerCap, null); } public static List<WarmRequest> Plan(IReadOnlyList<string>? prioritySpecies, IReadOnlyList<PeerWarmRow>? peers, int perPeerCap, Func<int, string, bool>? isAbandoned) { List<WarmRequest> list = new List<WarmRequest>(); if (prioritySpecies == null || prioritySpecies.Count == 0) { return list; } if (peers == null || peers.Count == 0) { return list; } List<string> list2 = new List<string>(); HashSet<string> hashSet = new HashSet<string>(KeyComparer); for (int i = 0; i < prioritySpecies.Count; i++) { string text = Norm(prioritySpecies[i]); if (text.Length != 0 && hashSet.Add(text)) { list2.Add(text); } } if (list2.Count == 0) { return list; } Dictionary<int, int> dictionary = new Dictionary<int, int>(); for (int j = 0; j < list2.Count; j++) { string text2 = list2[j]; for (int k = 0; k < peers.Count; k++) { PeerWarmRow peerWarmRow = peers[k]; if (peerWarmRow.Status != PeerWarmStatus.Participating || SpeciesRoomPolicy.Contains(peerWarmRow.Warm, text2) || SpeciesRoomPolicy.Contains(peerWarmRow.DeadEnds, text2) || peerWarmRow.RemainingBudget <= 0) { continue; } if (isAbandoned != null) { bool flag; try { flag = isAbandoned(peerWarmRow.Actor, text2); } catch { flag = false; } if (flag) { continue; } } dictionary.TryGetValue(peerWarmRow.Actor, out var value); if (perPeerCap <= 0 || value < perPeerCap) { dictionary[peerWarmRow.Actor] = value + 1; list.Add(new WarmRequest { Actor = peerWarmRow.Actor, Species = text2 }); } } } return list; } } public static class RootScale { public const float DegenerateBelow = 0.1f; public static bool IsDegenerate(float v) { return Math.Abs(v) < 0.1f; } public static bool NeedsFix(float x, float y, float z) { if (!IsDegenerate(x) && !IsDegenerate(y)) { return IsDegenerate(z); } return true; } public static void Fix(float x, float y, float z, out float ox, out float oy, out float oz) { float num = 0f; if (!IsDegenerate(x) && Math.Abs(x) > num) { num = Math.Abs(x); } if (!IsDegenerate(y) && Math.Abs(y) > num) { num = Math.Abs(y); } if (!IsDegenerate(z) && Math.Abs(z) > num) { num = Math.Abs(z); } if (num <= 0f) { num = 1f; } ox = (IsDegenerate(x) ? num : x); oy = (IsDegenerate(y) ? num : y); oz = (IsDegenerate(z) ? num : z); } } public static class SpawnCap { public const int DefaultCacheWarnThreshold = 8; public static IReadOnlyList<string> PickEvict(IReadOnlyList<string> lruOrder, int count, int max, ICollection<string>? exclude = null) { List<string> list = new List<string>(); if (max <= 0 || count <= max) { return list; } int num = count - max; for (int i = 0; i < lruOrder.Count; i++) { if (list.Count >= num) { break; } string item = lruOrder[i]; if (exclude == null || !exclude.Contains(item)) { list.Add(item); } } return list; } public static bool ShouldWarnCacheSize(int count, int threshold) { if (threshold > 0 && count > 0) { return count % threshold == 0; } return false; } public static bool IsCacheLarge(int count, int threshold) { if (threshold > 0) { return count >= threshold; } return false; } } public sealed class SpawnCountArgs { public string Species = ""; public int Count = 1; public static SpawnCountArgs Parse(string[] tokens) { SpawnCountArgs spawnCountArgs = new SpawnCountArgs(); int num = 1; int num2 = tokens.Length; if (num2 - num > 1 && int.TryParse(tokens[num2 - 1], out var result) && result > 0) { spawnCountArgs.Count = Math.Min(result, 99); num2--; } spawnCountArgs.Species = string.Join(" ", tokens, num, Math.Max(0, num2 - num)).Trim(); return spawnCountArgs; } } public static class SpawnMenuLabels { public static bool IsDonorMismatch(string key, string donorName) { if (!string.IsNullOrEmpty(donorName) && !string.IsNullOrEmpty(key)) { return !string.Equals(key.Trim(), donorName.Trim(), StringComparison.OrdinalIgnoreCase); } return false; } public static string RowLabel(string key, string resolvedDonorName, bool expeditionOnly) { string text = key ?? ""; if (IsDonorMismatch(key, resolvedDonorName)) { text = text + " → spawns “" + resolvedDonorName.Trim() + "”"; } if (expeditionOnly) { text += " (expedition-only — see log)"; } return text; } public static string RoomGlyph(bool localWarm, int participating, int coldOnPeers) { if (!localWarm) { return "○"; } if (participating <= 0 || coldOnPeers <= 0) { return "●"; } return "◐"; } public static string OwnerBreakdown(IEnumerable<string?>? ownerTags) { if (ownerTags == null) { return ""; } Dictionary<string, int> counts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); Dictionary<string, int> seenAt = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); List<string> list = new List<string>(); foreach (string ownerTag in ownerTags) { string text = SpawnPolicy.NormalizeOwnerTag(ownerTag); if (text.Length == 0) { text = "(untagged)"; } if (counts.ContainsKey(text)) { counts[text]++; continue; } counts[text] = 1; seenAt[text] = list.Count; list.Add(text); } if (list.Count == 0) { return ""; } list.Sort(delegate(string a, string b) { int num = counts[b].CompareTo(counts[a]); return (num == 0) ? seenAt[a].CompareTo(seenAt[b]) : num; }); StringBuilder stringBuilder = new StringBuilder(); foreach (string item in list) { if (stringBuilder.Length > 0) { stringBuilder.Append(", "); } stringBuilder.Append(item).Append(' ').Append(counts[item]); } return stringBuilder.ToString(); } } public static class SpawnNetProtocol { public struct SpawnMsg { public int Proto; public string SpeciesKey; public string Uid; public int ViewId; public string Scene; public float X; public float Y; public float Z; public float YawDeg; public int Faction; public bool StripQuestEvents; public string ConsumerData; public int RightHandItemId; public string RightHandItemUid; public int LeftHandItemId; public string LeftHandItemUid; } public enum GoneKind { Died, Despawned, Corpse } public struct GoneMsg { public string Uid; public GoneKind Kind; } public struct AckMsg { public string Uid; public string Source; public int TookMs; } public struct FailMsg { public string Uid; public string Reason; } public enum MirrorGate { Mirror, DropDuplicate, DropTimeout, WaitNotReady, WaitSceneMismatch, DropNotReadyTimeout, RefuseColdUnsafe } public const int Version = 1; public const string VerbSpawn = "sk.spawn"; public const string VerbGone = "sk.gone"; public const string VerbCorpse = "sk.corpse"; public const string VerbAck = "sk.ack"; public const string VerbFail = "sk.fail"; public const string VerbResync = "sk.resync"; public const string VerbTest = "sk.test"; public const string VerbWarmSet = "sk.warmset"; public const string VerbWarmClr = "sk.warmclr"; public const string VerbWant = "sk.want"; public const string StoreReleaseReasonPrefix = "owner release: "; public const string FailReasonNotReady = "not-ready"; public const string FailReasonColdUnsafe = "cold-unsafe"; public static string EncodeSpawn(SpawnMsg m) { return Join(I(m.Proto), Esc(m.SpeciesKey), Esc(m.Uid), I(m.ViewId), Esc(m.Scene), F(m.X), F(m.Y), F(m.Z), F(m.YawDeg), I(m.Faction), m.StripQuestEvents ? "1" : "0", Esc(m.ConsumerData), I(m.RightHandItemId), Esc(m.RightHandItemUid), I(m.LeftHandItemId), Esc(m.LeftHandItemUid)); } public static string EncodeGone(GoneMsg m) { return Join(Esc(m.Uid), I((int)m.Kind)); } public static string EncodeAck(AckMsg m) { return Join(Esc(m.Uid), Esc(m.Source), I(m.TookMs)); } public static string EncodeFail(FailMsg m) { return Join(Esc(m.Uid), Esc(m.Reason)); } public static bool TryDecodeSpawn(string payload, out SpawnMsg m) { m = default(SpawnMsg); List<string> list = Split(payload); if (list == null || list.Count < 11) { return false; } if (!TryI(list[0], out m.Proto)) { return false; } m.SpeciesKey = list[1]; m.Uid = list[2]; if (!TryI(list[3], out m.ViewId)) { return false; } m.Scene = list[4]; if (!TryF(list[5], out m.X) || !TryF(list[6], out m.Y) || !TryF(list[7], out m.Z)) { return false; } if (!TryF(list[8], out m.YawDeg)) { return false; } if (!TryI(list[9], out m.Faction)) { return false; } m.StripQuestEvents = list[10] != "0"; m.ConsumerData = ((list.Count >= 12) ? list[11] : ""); if (list.Count >= 13 && TryI(list[12], out var v)) { m.RightHandItemId = v; } m.RightHandItemUid = ((list.Count >= 14) ? list[13] : ""); if (list.Count >= 15 && TryI(list[14], out var v2)) { m.LeftHandItemId = v2; } m.LeftHandItemUid = ((list.Count >= 16) ? list[15] : ""); if (!string.IsNullOrEmpty(m.Uid) && m.ViewId > 0) { return !string.IsNullOrEmpty(m.SpeciesKey); } return false; } public static bool TryDecodeGone(string payload, out GoneMsg m) { m = default(GoneMsg); List<string> list = Split(payload); if (list == null || list.Count < 2) { return false; } m.Uid = list[0]; if (!TryI(list[1], out var v)) { return false; } if (v < 0 || v > 2) { return false; } m.Kind = (GoneKind)v; return !string.IsNullOrEmpty(m.Uid); } public static bool TryDecodeAck(string payload, out AckMsg m) { m = default(AckMsg); List<string> list = Split(payload); if (list == null || list.Count < 3) { return false; } m.Uid = list[0]; m.Source = list[1]; if (!TryI(list[2], out m.TookMs)) { return false; } return !string.IsNullOrEmpty(m.Uid); } public static bool TryParseGoneReason(string reason, out GoneMsg m) { m = default(GoneMsg); if (string.IsNullOrEmpty(reason)) { return false; } return TryDecodeGone(reason.StartsWith("owner release: ", StringComparison.Ordinal) ? reason.Substring("owner release: ".Length) : reason, out m); } public static bool TryDecodeFail(string payload, out FailMsg m) { m = default(FailMsg); List<string> list = Split(payload); if (list == null || list.Count < 2) { return false; } m.Uid = list[0]; m.Reason = list[1]; return !string.IsNullOrEmpty(m.Uid); } public static MirrorGate DecideMirror(bool alreadyKnown, bool playerReady, bool loadingDone, string activeScene, string payloadScene, float queuedSeconds, float timeoutSeconds, bool templateCached = true, bool harvestSafe = true) { if (alreadyKnown) { return MirrorGate.DropDuplicate; } bool flag = playerReady && loadingDone; bool flag2 = string.Equals(activeScene ?? "", payloadScene ?? "", StringComparison.Ordinal); if (queuedSeconds > timeoutSeconds) { if (!(flag && flag2)) { return MirrorGate.DropNotReadyTimeout; } return MirrorGate.DropTimeout; } if (!flag) { return MirrorGate.WaitNotReady; } if (!flag2) { return MirrorGate.WaitSceneMismatch; } if (!templateCached && !harvestSafe) { return MirrorGate.RefuseColdUnsafe; } return MirrorGate.Mirror; } private static string I(int v) { return v.ToString(CultureInfo.InvariantCulture); } private static string F(float v) { return v.ToString("R", CultureInfo.InvariantCulture); } private static bool TryI(string s, out int v) { return int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v); } private static bool TryF(string s, out float v) { return float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out v); } private static string Join(params string[] fields) { return string.Join(";", fields); } private static string Esc(string s) { if (string.IsNullOrEmpty(s)) { return ""; } StringBuilder stringBuilder = new StringBuilder(s.Length + 4); foreach (char c in s) { switch (c) { case '\\': stringBuilder.Append("\\\\"); break; case ';': stringBuilder.Append("\\s"); break; case '\n': stringBuilder.Append("\\n"); break; default: stringBuilder.Append(c); break; } } return stringBuilder.ToString(); } private static List<string> Split(string payload) { if (payload == null) { return null; } List<string> list = new List<string>(); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < payload.Length; i++) { char c = payload[i]; switch (c) { case '\\': if (i + 1 >= payload.Length) { return null; } switch (payload[++i]) { case '\\': stringBuilder.Append('\\'); break; case 's': stringBuilder.Append(';'); break; case 'n': stringBuilder.Append('\n'); break; default: return null; } break; case ';': list.Add(stringBuilder.ToString()); stringBuilder.Length = 0; break; default: stringBuilder.Append(c); break; } } list.Add(stringBuilder.ToString()); return list; } } public static class SpawnPolicy { public enum RoomSpawnDecision { Solo, CoopReady, CoopDegraded, ProceedGhost, RefusePeersNotReady, RefuseLegacy } public const float MinDistance = 1f; public const float MaxDistance = 50f; public static float EffectiveDistance(float? requested, float configured) { if (!requested.HasValue) { return configured; } float value = requested.Value; if (value < 1f) { return 1f; } if (value > 50f) { return 50f; } return value; } public static string NormalizeOwnerTag(string? tag) { return tag?.Trim() ?? ""; } public static bool MatchesOwner(string recordTag, string? filter) { if (filter != null) { return string.Equals(NormalizeOwnerTag(recordTag), NormalizeOwnerTag(filter), StringComparison.OrdinalIgnoreCase); } return true; } public static bool RefuseRoomSpawn(bool inRoom, int otherPlayerCount, bool allowSpawnInRoom) { if (inRoom && otherPlayerCount > 0) { return !allowSpawnInRoom; } return false; } public static RoomSpawnDecision DecideRoomSpawn(bool inRoom, int otherPlayerCount, bool coopEnabled, int peersWithoutHello, bool allowSpawnInRoom) { if (!inRoom || otherPlayerCount <= 0) { return RoomSpawnDecision.Solo; } if (!coopEnabled) { if (!allowSpawnInRoom) { return RoomSpawnDecision.RefuseLegacy; } return RoomSpawnDecision.ProceedGhost; } if (peersWithoutHello <= 0) { return RoomSpawnDecision.CoopReady; } if (!allowSpawnInRoom) { return RoomSpawnDecision.RefusePeersNotReady; } return RoomSpawnDecision.CoopDegraded; } public static bool IsRefusal(RoomSpawnDecision d) { if (d != RoomSpawnDecision.RefusePeersNotReady) { return d == RoomSpawnDecision.RefuseLegacy; } return true; } public static bool ShouldBroadcast(RoomSpawnDecision d) { if (d != RoomSpawnDecision.CoopReady) { return d == RoomSpawnDecision.CoopDegraded; } return true; } } public static class SpawnUid { public const string Prefix = "SK_"; public const string ItemPrefix = "SKi_"; public static string Mint(Guid guid) { return "SK_" + guid.ToString("N"); } public static string MintItem(Guid guid) { return "SKi_" + guid.ToString("N"); } public static bool IsSpawnUid(string? uid) { if (uid != null && uid.StartsWith("SK_", StringComparison.Ordinal)) { return uid.Length > "SK_".Length; } return false; } public static bool IsSpawnItemUid(string? uid) { if (uid != null && uid.StartsWith("SKi_", StringComparison.Ordinal)) { return uid.Length > "SKi_".Length; } return false; } } public sealed class SpawnVerbArgs { public string Species = ""; public float? Distance; public float? LifetimeSeconds; public string? Faction; public string? OwnerTag; public string? Body; public float? CorpseLingerSeconds; public bool KeepQuestEvents; public bool IgnoreRoomWarm; public readonly List<string> UnknownOptions = new List<string>(); public static SpawnVerbArgs Parse(string[] tokens) { SpawnVerbArgs spawnVerbArgs = new SpawnVerbArgs(); List<string> list = new List<string>(); for (int i = 1; i < tokens.Length; i++) { string text = tokens[i]; if (string.IsNullOrWhiteSpace(text)) { continue; } if (string.Equals(text, "keepquest", StringComparison.OrdinalIgnoreCase)) { spawnVerbArgs.KeepQuestEvents = true; continue; } if (string.Equals(text, "force", StringComparison.OrdinalIgnoreCase)) { spawnVerbArgs.IgnoreRoomWarm = true; continue; } int num = text.IndexOf('='); if (num <= 0) { list.Add(text); continue; } string text2 = text.Substring(0, num).Trim().ToLowerInvariant(); string text3 = text.Substring(num + 1).Trim(); switch (text2) { case "dist": spawnVerbArgs.Distance = ParseFloat(text3, spawnVerbArgs, text); break; case "life": spawnVerbArgs.LifetimeSeconds = ParseFloat(text3, spawnVerbArgs, text); break; case "faction": spawnVerbArgs.Faction = text3; break; case "owner": spawnVerbArgs.OwnerTag = text3; break; case "body": spawnVerbArgs.Body = text3; break; case "linger": spawnVerbArgs.CorpseLingerSeconds = ParseFloat(text3, spawnVerbArgs, text); break; default: spawnVerbArgs.UnknownOptions.Add(text); break; } } spawnVerbArgs.Species = string.Join(" ", list).Trim(); return spawnVerbArgs; } private static float? ParseFloat(string val, SpawnVerbArgs result, string token) { if (float.TryParse(val, NumberStyles.Float, CultureInfo.InvariantCulture, out var result2)) { return result2; } result.UnknownOptions.Add(token); return null; } } public static class SpawnWatch { public const float DefaultIntervalSeconds = 0.5f; public static WatchState Next(WatchState current, bool exists, bool alive) { if (current != WatchState.Alive) { return current; } if (!exists) { return WatchState.Despawned; } if (!alive) { return WatchState.Died; } return WatchState.Alive; } public static bool IsTerminal(WatchState state) { if (state != WatchState.Died && state != WatchState.Despawned) { return state == WatchState.Failed; } return true; } } public enum WatchState { Pending, Alive, Died, Despawned, Failed } public static class SpeciesFilter { public static IReadOnlyList<string> Apply(IReadOnlyList<string> keys, string? query, Func<string, string?>? resolvedNameOf) { if (keys == null) { return Array.Empty<string>(); } string text = query?.Trim() ?? ""; if (text.Length == 0) { return keys; } List<string> list = new List<string>(); for (int i = 0; i < keys.Count; i++) { string text2 = keys[i]; if (Matches(text2, text)) { list.Add(text2); } else if (Matches(resolvedNameOf?.Invoke(text2), text)) { list.Add(text2); } } return list; } private static bool Matches(string? text, string query) { if (!string.IsNullOrEmpty(text)) { return text.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; } return false; } public static bool CancelDown(bool escDown, bool padCancelDown) { return escDown || padCancelDown; } public static bool EscConsumesFilter(bool open, bool hasQuery, bool escDown) { return open && hasQuery && escDown; } } public enum PeerWarmStatus { Unmodded, HelloedNoRow, Participating } public struct PeerWarmRow { public int Actor; public PeerWarmStatus Status; public string[] Warm; public string[] DeadEnds; public int RemainingBudget; } public enum RoomWarmMode { Local, RoomDegraded, RoomStrict } public enum RoomWarmVerdict { Spawnable, ColdOnPeer, DeadEndOnPeer, ColdLocally } public struct RoomWarmDecision { public RoomWarmVerdict Verdict; public int[] Actors; public string Reason; public bool Degraded; } public static class SpeciesRoomPolicy { private static readonly StringComparer KeyComparer = StringComparer.OrdinalIgnoreCase; public const string ReasonColdLocally = "cold-locally"; public const string ReasonColdOnPeer = "cold-on-peer"; public const string ReasonDeadEndOnPeer = "dead-end-on-peer"; private static readonly int[] EmptyActors = new int[0]; private static string Norm(string key) { if (key != null) { return key.Trim(); } return ""; } public static bool Contains(string[] keys, string key) { return ContainsNorm(keys, Norm(key)); } private static bool ContainsNorm(string[] keys, string key) { if (keys == null) { return false; } for (int i = 0; i < keys.Length; i++) { if (KeyComparer.Equals(Norm(keys[i]), key)) { return true; } } return false; } public static RoomWarmDecision Decide(string speciesKey, bool localWarm, IReadOnlyList<PeerWarmRow> peers, RoomWarmMode mode) { if (!localWarm) { return new RoomWarmDecision { Verdict = RoomWarmVerdict.ColdLocally, Actors = EmptyActors, Reason = "cold-locally", Degraded = false }; } if (mode == RoomWarmMode.Local || peers == null || peers.Count == 0) { return Spawnable(); } string key = Norm(speciesKey); List<int> list = new List<int>(); List<int> list2 = new List<int>(); for (int i = 0; i < peers.Count; i++) { PeerWarmRow peerWarmRow = peers[i]; if (peerWarmRow.Status == PeerWarmStatus.Participating && !ContainsNorm(peerWarmRow.Warm, key)) { if (ContainsNorm(peerWarmRow.DeadEnds, key) || peerWarmRow.RemainingBudget == 0) { list2.Add(peerWarmRow.Actor); } else { list.Add(peerWarmRow.Actor); } } } if (list.Count == 0 && list2.Count == 0) { return Spawnable(); } List<int> list3 = new List<int>(list2.Count + list.Count); list3.AddRange(list2); list3.AddRange(list); list3.Sort(); RoomWarmDecision result = new RoomWarmDecision { Verdict = ((list2.Count <= 0) ? RoomWarmVerdict.ColdOnPeer : RoomWarmVerdict.DeadEndOnPeer), Actors = list3.ToArray(), Reason = ((list2.Count > 0) ? "dead-end-on-peer" : "cold-on-peer"), Degraded = false }; if (mode == RoomWarmMode.RoomDegraded) { result.Verdict = RoomWarmVerdict.Spawnable; result.Degraded = true; } return result; } private static RoomWarmDecision Spawnable() { return new RoomWarmDecision { Verdict = RoomWarmVerdict.Spawnable, Actors = EmptyActors, Reason = "", Degraded = false }; } public static List<string> Intersection(IEnumerable<string> localWarm, IReadOnlyList<PeerWarmRow> peers) { List<string> list = new List<string>(); if (localWarm == null) { return list; } HashSet<string> hashSet = new HashSet<string>(KeyComparer); foreach (string item in localWarm) { string text = Norm(item); if (text.Length == 0 || !hashSet.Add(text)) { continue; } bool flag = true; if (peers != null) { for (int i = 0; i < peers.Count && flag; i++) { if (peers[i].Status == PeerWarmStatus.Participating && !ContainsNorm(peers[i].Warm, text)) { flag = false; } } } if (flag) { list.Add(text); } } list.Sort(StringComparer.Ordinal); return list; } public static bool IsVeto(RoomWarmDecision d) { return d.Verdict != RoomWarmVerdict.Spawnable; } public static bool IsDeadEnd(RoomWarmDecision d) { return string.Equals(d.Reason, "dead-end-on-peer", StringComparison.Ordinal); } } public static class TemplateAiGate { public static bool RefusesAdoption(bool hasCharacterAI, bool aiPrefabSet) { if (hasCharacterAI) { return !aiPrefabSet; } return true; } public static bool RefusesAdoption(bool hasCharacterAI, bool aiPrefabSet, bool isGuest, int aiRootCount) { if (isGuest && !hasCharacterAI && aiRootCount >= 1) { return false; } return RefusesAdoption(hasCharacterAI, aiPrefabSet); } public static string Reason(string speciesKey, string origin) { return "'" + speciesKey + "' (" + origin + ") has no CharacterAI with an AIStatesPrefab — CharacterAI.GetAIStates builds its state graph ONLY from AIStatesPrefab (an AIRoot child alone never yields states), so an enemy minted from it can never have AI (BUG-PREBUILTADOPT)"; } } public static class VisualGate { public enum Action { None, ForceVisuals, Rebind, Both } public struct Census { public int Renderers; public int RenderReady; public int SkinnedReady; public bool AnimatorPresent; public bool AnimatorInitialized; public float BakedX; public float BakedY; public float BakedZ; public int StackedBones; public int TotalBones; } public const float DefaultDegenerateAxisRatio = 0.05f; public const float DefaultStackedFraction = 0.8f; public static bool DegenerateBake(in Census c, float degenerateAxisRatio = 0.05f) { float num = Min3(c.BakedX, c.BakedY, c.BakedZ); float num2 = Max3(c.BakedX, c.BakedY, c.BakedZ); if (num2 > 0f) { return num / num2 <= degenerateAxisRatio; } return false; } public static bool NoUsableBody(in Census c, float degenerateAxisRatio = 0.05f) { if (c.Renderers != 0) { return DegenerateBake(in c, degenerateAxisRatio); } return true; } public static Action Decide(in Census c, float degenerateAxisRatio, float stackedFraction, out string reason) { bool flag = c.Renderers == 0 || c.RenderReady == 0 || c.SkinnedReady == 0; bool flag2 = DegenerateBake(in c, degenerateAxisRatio); bool flag3 = c.TotalBones > 0 && (float)c.StackedBones / (float)c.TotalBones >= stackedFraction; bool flag4 = c.AnimatorPresent && (!c.AnimatorInitialized || flag2 || flag3); reason = ((!flag) ? "" : ((c.Renderers == 0) ? "no renderers (Start-built visuals never ran)" : ((c.RenderReady == 0) ? "zero render-ready renderers" : "zero render-ready SKINNED renderers (only FX draw)"))); if (flag4) { string text = ((!c.AnimatorInitialized) ? "animator not initialized" : (flag2 ? ("degenerate baked bounds (" + F(c.BakedX) + ", " + F(c.BakedY) + ", " + F(c.BakedZ) + ")") : $"unposed skeleton (stacked={c.StackedBones}/{c.TotalBones})")); reason = ((reason.Length > 0) ? (reason + "; " + text) : text); } if (!flag && !flag4) { reason = "healthy"; } if (flag && flag4) { return Action.Both; } if (flag) { return Action.ForceVisuals; } if (flag4) { return Action.Rebind; } return Action.None; } public static Action Decide(in Census c, out string reason) { return Decide(in c, 0.05f, 0.8f, out reason); } private static float Min3(float a, float b, float c) { if (!(a < b)) { if (!(b < c)) { return c; } return b; } if (!(a < c)) { return c; } return a; } private static float Max3(float a, float b, float c) { if (!(a > b)) { if (!(b > c)) { return c; } return b; } if (!(a > c)) { return c; } return a; } private static string F(float v) { return v.ToString("0.0#", CultureInfo.InvariantCulture); } } public enum WantStep { Send, Wait, Retired, GiveUpColdOnPeer, GiveUpDeadEnd } public struct WantEntry { public int Actor; public string Species; public int Attempts; public double NextSendAt; } public sealed class WantBook { public const int MaxPerActor = 32; private static readonly StringComparer KeyComparer = StringComparer.OrdinalIgnoreCase; private readonly List<WantEntry> _entries = new List<WantEntry>(); private readonly List<WantEntry> _tombstones = new List<WantEntry>(); private readonly double _firstTimeout; private readonly double _maxTimeout; private readonly int _maxAttempts; public int Count => _entries.Count; public int AbandonedCount => _tombstones.Count; public WantBook(double firstTimeoutSeconds = 30.0, double maxTimeoutSeconds = 120.0, int maxAttempts = 3) { _firstTimeout = ((firstTimeoutSeconds > 0.0) ? firstTimeoutSeconds : 30.0); _maxTimeout = ((maxTimeoutSeconds >= _firstTimeout) ? maxTimeoutSeconds : _firstTimeout); _maxAttempts = ((maxAttempts <= 0) ? 1 : maxAttempts); } private static string Norm(string key) { if (key != null) { return key.Trim(); } return ""; } private int IndexOf(int actor, string normKey) { for (int i = 0; i < _entries.Count; i++) { if (_entries[i].Actor == actor && KeyComparer.Equals(Norm(_entries[i].Species), normKey)) { return i; } } return -1; } private int TombstoneIndexOf(int actor, string normKey) { for (int i = 0; i < _tombstones.Count; i++) { if (_tombstones[i].Actor == actor && KeyComparer.Equals(Norm(_tombstones[i].Species), normKey)) { return i; } } return -1; } public bool IsAbandoned(int actor, string species) { string text = Norm(species); if (text.Length != 0) { return TombstoneIndexOf(actor, text) >= 0; } return false; } public List<WantEntry> Tombstones() { return new List<WantEntry>(_tombstones); } public void CopyTombstonesFrom(WantBook other) { if (other == null || other == this) { return; } for (int i = 0; i < other._tombstones.Count; i++) { WantEntry item = other._tombstones[i]; if (TombstoneIndexOf(item.Actor, Norm(item.Species)) < 0) { _tombstones.Add(item); } } } public bool Want(int actor, string species, double now) { string text = Norm(species); if (text.Length == 0) { return false; } if (IndexOf(actor, text) >= 0) { return true; } if (TombstoneIndexOf(actor, text) >= 0) { return false; } int num = 0; for (int i = 0; i < _entries.Count; i++) { if (_entries[i].Actor == actor) { num++; } } while (num >= 32) { for (int j = 0; j < _entries.Count; j++) { if (_entries[j].Actor == actor) { _entries.RemoveAt(j); break; } } num--; } _entries.Add(new WantEntry { Actor = actor, Species = text, Attempts = 0, NextSendAt = now }); return true; } private double Timeout(int attemptNumber) { double num = _firstTimeout; for (int i = 1; i < attemptNumber; i++) { num *= 2.0; if (num >= _maxTimeout) { return _maxTimeout; } } if (!(num < _maxTimeout)) { return _maxTimeout; } return num; } public WantStep Step(int actor, string species, bool peerWarmNow, bool peerDeadEnd, int peerBudget, double now) { string text = Norm(species); if (text.Length == 0) { return WantStep.Retired; } if (peerWarmNow) { int num = TombstoneIndexOf(actor, text); if (num >= 0) { _tombstones.RemoveAt(num); } } int num2 = IndexOf(actor, text); if (num2 < 0) { return WantStep.Retired; } if (peerWarmNow) { _entries.RemoveAt(num2); return WantStep.Retired; } if (peerDeadEnd || peerBudget <= 0) { _entries.RemoveAt(num2); return WantStep.GiveUpDeadEnd; } WantEntry wantEntry = _entries[num2]; if (wantEntry.Attempts == 0) { wantEntry.Attempts = 1; wantEntry.NextSendAt = now + Timeout(1); _entries[num2] = wantEntry; return WantStep.Send; } if (now < wantEntry.NextSendAt) { return WantStep.Wait; } if (wantEntry.Attempts < _maxAttempts) { wantEntry.Attempts++; wantEntry.NextSendAt = now + Timeout(wantEntry.Attempts); _entries[num2] = wantEntry; return WantStep.Send; } _entries.RemoveAt(num2); if (TombstoneIndexOf(actor, text) < 0) { _tombstones.Add(wantEntry); } return WantStep.GiveUpColdOnPeer; } public List<WantEntry> TakeDue(Func<int, string, (bool warm, bool dead, int budget)?> peerFacts, double now, out List<WantEntry> gaveUp) { List<WantEntry> list = new List<WantEntry>(); gaveUp = new List<WantEntry>(); if (peerFacts == null) { return list; } List<int> list2 = new List<int>(); for (int i = 0; i < _entries.Count; i++) { list2.Add(i); } list2.Sort(delegate(int a, int b) { int num3 = _entries[a].Actor.CompareTo(_entries[b].Actor); return (num3 == 0) ? a.CompareTo(b) : num3; }); List<WantEntry> list3 = new List<WantEntry>(list2.Count); foreach (int item in list2) { list3.Add(_entries[item]); } foreach (WantEntry item2 in list3) { (bool, bool, int)? tuple; try { tuple = peerFacts(item2.Actor, item2.Species); } catch { continue; } if (!tuple.HasValue) { continue; } switch (Step(item2.Actor, item2.Species, tuple.Value.Item1, tuple.Value.Item2, tuple.Value.Item3, now)) { case WantStep.Send: { int num = IndexOf(item2.Actor, Norm(item2.Species)); if (num >= 0) { list.Add(_entries[num]); } break; } case WantStep.GiveUpColdOnPeer: case WantStep.GiveUpDeadEnd: gaveUp.Add(item2); break; } } List<WantEntry> list4 = new List<WantEntry>(_tombstones); for (int num2 = 0; num2 < list4.Count; num2++) { (bool, bool, int)? tuple2; try { tuple2 = peerFacts(list4[num2].Actor, list4[num2].Species); } catch { continue; } if (tuple2.HasValue && tuple2.Value.Item1) { Step(list4[num2].Actor, list4[num2].Species, peerWarmNow: true, tuple2.Value.Item2, tuple2.Value.Item3, now); } } return list; } public void ForgetActor(int actor) { for (int num = _entries.Count - 1; num >= 0; num--) { if (_entries[num].Actor == actor) { _entries.RemoveAt(num); } } for (int num2 = _tombstones.Count - 1; num2 >= 0; num2--) { if (_tombstones[num2].Actor == actor) { _tombstones.RemoveAt(num2); } } } public void Clear() { _entries.Clear(); _tombstones.Clear(); } public List<WantEntry> Snapshot() { return new List<WantEntry>(_entries); } } public struct WarmSet { public bool IsPresent; public string[] Warm; public string[] DeadEnds; public int RemainingBudget; } public static class WarmSetWire { public const string Version = "1"; public const int UnlimitedBudgetSentinel = 99; public const int MaxKeysPerField = 256; private static readonly string[] NoKeys = new string[0]; private static readonly StringComparer KeyComparer = StringComparer.OrdinalIgnoreCase; public static int PublishedBudget(int hostRemaining, int ceilingRemaining) { int num = ((hostRemaining < 0) ? 99 : Math.Min(99, hostRemaining)); if (ceilingRemaining < 0) { return num; } return Math.Min(num, Math.Max(0, ceilingRemaining)); } public static string Encode(IEnumerable<string> warm, IEnumerable<string> deadEnds, int remainingBudget) { if (remainingBudget < 0) { remainingBudget = 0; } StringBuilder stringBuilder = new StringBuilder(64); stringBuilder.Append("1"); stringBuilder.Append("|w="); AppendKeys(stringBuilder, Canonical(warm)); stringBuilder.Append("|d="); AppendKeys(stringBuilder, Canonical(deadEnds)); stringBuilder.Append("|b="); stringBuilder.Append(remainingBudget.ToString(CultureInfo.InvariantCulture)); return stringBuilder.ToString(); } public static WarmSet Decode(string payload) { WarmSet result = new WarmSet { IsPresent = false, Warm = NoKeys, DeadEnds = NoKeys, RemainingBudget = 0 }; if (string.IsNullOrEmpty(payload)) { return result; } List<string> list = SplitTop(payload); if (list == null || list.Count < 1) { return result; } if (list[0] != "1") { return result; } string[] array = null; string[] array2 = null; int result2 = 0; bool flag = false; for (int i = 1; i < list.Count; i++) { string text = list[i]; if (text.Length >= 2 && text[0] == 'w' && text[1] == '=') { if (array != null) { return result; } array = ParseKeys(text.Substring(2)); if (array == null || array.Length > 256) { return result; } } else if (text.Length >= 2 && text[0] == 'd' && text[1] == '=') { if (array2 != null) { return result; } array2 = ParseKeys(text.Substring(2)); if (array2 == null || array2.Length > 256) { return result; } } else if (text.Length >= 2 && text[0] == 'b' && text[1] == '=') { if (flag) { return result; } if (!int.TryParse(text.Substring(2), NumberStyles.Integer, CultureInfo.InvariantCulture, out result2)) { return result; } if (result2 < 0) { result2 = 0; } flag = true; } } if (array == null || array2 == null || !flag) { return result; } return new WarmSet { IsPresent = true, Warm = array, DeadEnds = array2, RemainingBudget = result2 }; } public static bool SetEquals(WarmSet a, WarmSet b) { if (a.IsPresent != b.IsPresent) { return false; } if (!a.IsPresent) { return true; } if (a.RemainingBudget != b.RemainingBudget) { return false; } if (KeysEqual(a.Warm, b.Warm)) { return KeysEqual(a.DeadEnds, b.DeadEnds); } return false; } public static bool Contains(WarmSet s, string speciesKey) { if (!s.IsPresent || s.Warm == null) { return false; } string text = ((speciesKey == null) ? "" : speciesKey.Trim()); if (text.Length == 0) { return false; } for (int i = 0; i < s.Warm.Length; i++) { if (KeyComparer.Equals(s.Warm[i], text)) { return true; } } return false; } private static List<string> Canonical(IEnumerable<string> keys) { List<string> list = new List<string>(); if (keys != null) { foreach (string key in keys) { if (key != null) { string text = key.Trim(); if (text.Length != 0) { list.Add(text); } } } } list.Sort(CanonicalOrder); List<string> list2 = new List<string>(list.Count); for (int i = 0; i < list.Count; i++) { if (i == 0 || !KeyComparer.Equals(list[i], list[i - 1])) { list2.Add(list[i]); } } return list2; } private static void AppendKeys(StringBuilder sb, List<string> keys) { for (int i = 0; i < keys.Count; i++) { if (i != 0) { sb.Append(';'); } Esc(sb, keys[i]); } } private static void Esc(StringBuilder sb, string s) { foreach (char c in s) { switch (c) { case '\\': sb.Append("\\\\"); break; case ';': sb.Append("\\s"); break; case '|': sb.Append("\\p"); break; case '\n': sb.Append("\\n"); break; default: sb.Append(c); break; } } } private static List<string> SplitTop(string payload) { List<string> list = new List<string>(); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < payload.Length; i++) { char c = payload[i]; switch (c) { case '\\': if (i + 1 >= payload.Length) { return null; } stringBuilder.Append(c).Append(payload[++i]); break; case '|': list.Add(stringBuilder.ToString()); stringBuilder.Length = 0; break; default: stringBuilder.Append(c); break; } } list.Add(stringBuilder.ToString()); return list; } private static string[] ParseKeys(string body) { if (body.Length == 0) { return NoKeys; } List<string> list = new List<string>(); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < body.Length; i++) { char c = body[i]; switch (c) { case '\\': if (i + 1 >= body.Length) { return null; } switch (body[++i]) { case '\\': stringBuilder.Append('\\'); break; case 's': stringBuilder.Append(';'); break; case 'p': stringBuilder.Append('|'); break; case 'n': stringBuilder.Append('\n'); break; default: return null; } break; case ';': list.Add(stringBuilder.ToString()); stringBuilder.Length = 0; break; default: stringBuilder.Append(c); break; } } list.Add(stringBuilder.ToString()); return list.ToArray(); } private static int CanonicalOrder(string x, string y) { int num = string.Compare(x, y, StringComparison.OrdinalIgnoreCase); if (num == 0) { return string.CompareOrdinal(x, y); } return num; } private static bool KeysEqual(string[] a, string[] b) { a = a ?? NoKeys; b = b ?? NoKeys; if (a.Length != b.Length) { return false; } List<string> list = new List<string>(a); list.Sort(CanonicalOrder); List<string> list2 = new List<string>(b); list2.Sort(CanonicalOrder); for (int i = 0; i < list.Count; i++) { if (!KeyComparer.Equals(list[i], list2[i])) { return false; } } return true; } } }
plugins/SpawnKit.dll
Decompiled 6 days 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.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 CompanionKit; using CompanionKit.Core; using DonorKit; using ForgeKit; using HarmonyLib; using Microsoft.CodeAnalysis; using NetKit; using NetKit.Core; using SpawnKit.Core; using UnityEngine; using UnityEngine.AI; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("SpawnKit")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.5.2.0")] [assembly: AssemblyInformationalVersion("0.5.2+091b206910305beb491301afe1db01b8cd7b8e72")] [assembly: AssemblyProduct("SpawnKit")] [assembly: AssemblyTitle("SpawnKit")] [assembly: AssemblyMetadata("BuildStamp", "091b2069 2026-08-28")] [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; } } } namespace SpawnKit { public static class BoneProbe { private static int sampled; public static void Dump(string nameFilter, float radius = 30f) { //IL_006d: 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_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) CharacterManager instance = CharacterManager.Instance; Character val = Lifecycle.FirstLocalCharacterOrNull(); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"[BONES] no local player — nothing to probe."); return; } DictionaryExt<string, Character> characters = instance.Characters; StringBuilder stringBuilder = new StringBuilder(); int num = 0; for (int i = 0; i < characters.Count; i++) { Character val2 = characters.Values[i]; if ((Object)(object)val2 == (Object)null || (Object)(object)val2 == (Object)(object)val) { continue; } float num2 = Vector3.Distance(((Component)val2).transform.position, ((Component)val).transform.position); if (!(num2 > radius) && (string.IsNullOrEmpty(nameFilter) || (val2.Name != null && val2.Name.IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0))) { string text = "?"; try { text = ((object)val2.UID/*cast due to .constrained prefix*/).ToString(); } catch { } bool flag = SpawnUid.IsSpawnUid(text); stringBuilder.Append(string.Format("\n '{0}' uid={1} spawned={2} dist={3:0.0}m", val2.Name, text, flag ? "Y" : "N", num2)); stringBuilder.Append('\n').Append(SkeletonRig.Census(((Component)val2).gameObject)); num++; if (sampled < 3 && (Object)(object)Plugin.Instance != (Object)null) { sampled++; ((MonoBehaviour)Plugin.Instance).StartCoroutine(SampleBoneMotion(val2, text)); } } } Plugin.Log.LogMessage((object)((num == 0) ? string.Format("[BONES] no characters within {0:0}m{1}.", radius, string.IsNullOrEmpty(nameFilter) ? "" : (" matching '" + nameFilter + "'")) : string.Format("[BONES] {0} character(s) within {1:0}m{2}:{3}", num, radius, string.IsNullOrEmpty(nameFilter) ? "" : (" matching '" + nameFilter + "'"), stringBuilder))); } private static IEnumerator SampleBoneMotion(Character ch, string uid) { SkinnedMeshRenderer body = (((Object)(object)ch != (Object)null) ? VisualPass.LargestSmr(((Component)ch).gameObject) : null); if ((Object)(object)body == (Object)null) { sampled--; yield break; } Transform[] bones = body.bones; Quaternion[] rot = (Quaternion[])(object)new Quaternion[bones.Length]; Vector3[] pos = (Vector3[])(object)new Vector3[bones.Length]; for (int i = 0; i < bones.Length; i++) { if ((Object)(object)bones[i] != (Object)null) { rot[i] = bones[i].localRotation; pos[i] = bones[i].localPosition; } } for (int f = 0; f < 30; f++) { yield return null; } int num = 0; int num2 = 0; float num3 = 0f; float num4 = 0f; try { if ((Object)(object)body == (Object)null || (Object)(object)ch == (Object)null || (Object)(object)((Component)ch).gameObject == (Object)null) { sampled--; yield break; } bones = body.bones; for (int j = 0; j < bones.Length && j < rot.Length; j++) { if (!((Object)(object)bones[j] == (Object)null)) { num2++; float num5 = Quaternion.Angle(rot[j], bones[j].localRotation); float num6 = Vector3.Distance(pos[j], bones[j].localPosition); if (num5 > 0.5f || num6 > 0.005f) { num++; } if (num5 > num3) { num3 = num5; } if (num6 > num4) { num4 = num6; } } } Plugin.Log.LogMessage((object)($"[BONES] motion '{ch.Name}' uid={uid}: movedBones={num}/{num2} over 30 frames " + $"maxDelta={num3:0.#}° / {num4:0.###}m")); } finally { sampled--; } } } internal static class CancelInput { private static PropertyInfo _isReady; private static bool _probed; private static bool _disabled; private static int _throws; internal static bool Down() { if (_disabled) { return false; } try { if (!RewiredReady()) { return false; } return ControlsInput.MenuCancelSystem(); } catch (Exception ex) { if (++_throws >= 3) { _disabled = true; Debug.LogWarning((object)("[SpawnKit] Rewired menu-cancel poll disabled after 3 throws: " + ex.Message)); } return false; } } private static bool RewiredReady() { if (!_probed) { _probed = true; Type type = Type.GetType("Rewired.ReInput, Rewired_Core", throwOnError: false); _isReady = ((type == null) ? null : type.GetProperty("isReady", BindingFlags.Static | BindingFlags.Public)); } if (_isReady == null) { return false; } object value = _isReady.GetValue(null, null); bool flag = default(bool); int num; if (value is bool) { flag = (bool)value; num = 1; } else { num = 0; } return (byte)((uint)num & (flag ? 1u : 0u)) != 0; } } internal static class CasterSkills { internal static int EnsureLearned(Character ch) { int num = 0; try { CharacterSkillKnowledge knowledge = KnowledgeOf(ch); InstantiateStartingSkills(ch, ref knowledge); if ((Object)(object)knowledge == (Object)null) { return 0; } Skill[] componentsInChildren = ((Component)knowledge).GetComponentsInChildren<Skill>(true); List<int> list = new List<int>(); List<int> list2 = new List<int>(); Skill[] array = componentsInChildren; foreach (Skill val in array) { if ((Object)(object)val != (Object)null) { list.Add(((Item)val).ItemID); } } IList<Item> learnedItems = ((CharacterKnowledge)knowledge).GetLearnedItems(); if (learnedItems != null) { foreach (Item item in learnedItems) { if ((Object)(object)item != (Object)null) { list2.Add(item.ItemID); } } } List<int> list3 = CasterSkillGate.MissingSkillIds((IEnumerable<int>)list2, (IEnumerable<int>)list); Skill[] array2 = componentsInChildren; foreach (Skill val2 in array2) { if (!((Object)(object)val2 == (Object)null) && list3.Contains(((Item)val2).ItemID) && ForceLearn(ch, knowledge, val2)) { num++; list3.Remove(((Item)val2).ItemID); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SPAWN] caster skill-knowledge repair threw: " + ex.Message)); } return num; } internal static string StateFor(Character ch) { try { CharacterSkillKnowledge val = KnowledgeOf(ch); if ((Object)(object)val == (Object)null) { return CasterSkillGate.Census(0, 0, false); } int num = 0; Skill[] componentsInChildren = ((Component)val).GetComponentsInChildren<Skill>(true); foreach (Skill val2 in componentsInChildren) { if ((Object)(object)val2 != (Object)null) { num++; } } return CasterSkillGate.Census(((CharacterKnowledge)val).GetLearnedItems()?.Count ?? 0, num, true); } catch { return "skills=?/?"; } } private static CharacterSkillKnowledge KnowledgeOf(Character ch) { if (!((Object)(object)ch != (Object)null) || !((Object)(object)ch.Inventory != (Object)null)) { return null; } return ch.Inventory.SkillKnowledge; } private static void InstantiateStartingSkills(Character ch, ref CharacterSkillKnowledge knowledge) { //IL_00d8: Unknown result type (might be due to invalid IL or missing references) StartingEquipment val = (((Object)(object)ch != (Object)null) ? ((Component)ch).GetComponent<StartingEquipment>() : null); if ((Object)(object)val == (Object)null || val.StartingSkills == null || val.StartingSkills.Length == 0) { return; } if ((Object)(object)knowledge == (Object)null || !val.m_holderInitialized) { val.InitHolders(); knowledge = KnowledgeOf(ch); } if ((Object)(object)knowledge == (Object)null) { return; } HashSet<int> hashSet = new HashSet<int>(); Skill[] componentsInChildren = ((Component)knowledge).GetComponentsInChildren<Skill>(true); foreach (Skill val2 in componentsInChildren) { if ((Object)(object)val2 != (Object)null) { hashSet.Add(((Item)val2).ItemID); } } Skill[] startingSkills = val.StartingSkills; foreach (Skill val3 in startingSkills) { if ((Object)(object)val3 == (Object)null || !hashSet.Add(((Item)val3).ItemID)) { continue; } Skill val4 = null; try { val4 = Object.Instantiate<Skill>(val3); ((Item)val4).SaveType = (SaveTypes)2; ((Item)val4).UID = SpawnUid.MintItem(Guid.NewGuid()); ((Component)val4).transform.SetParent(((Component)knowledge).transform); UnityEngineExtensions.ResetLocal(((Component)val4).transform, true); val4.IgnoreLearnNotification = true; Plugin.Log.LogMessage((object)($"[SPAWN] instantiated StartingSkill {((Item)val3).ItemID} '{((Item)val3).Name}' on " + "'" + ((Object)((Component)ch).gameObject).name + "' (BUG-CASTERNOSKILL — vanilla InitSkills never re-fires on a mid-gameplay clone).")); } catch (Exception ex) { if ((Object)(object)val4 != (Object)null) { Object.Destroy((Object)(object)((Component)val4).gameObject); } Plugin.Log.LogWarning((object)("[SPAWN] StartingSkill instantiate '" + ((Object)val3).name + "' threw: " + ex.Message)); } } } private static bool ForceLearn(Character ch, CharacterSkillKnowledge knowledge, Skill sk) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) try { if (!((Component)sk).gameObject.activeSelf) { ((Component)sk).gameObject.SetActive(true); } ((Item)sk).SaveType = (SaveTypes)2; if ((Object)(object)((Item)sk).m_lastParentTrans == (Object)null) { ((Item)sk).m_lastParentTrans = ((Component)sk).transform.parent; } ((Item)sk).ForceUpdateParentChange(); if (!((CharacterKnowledge)knowledge).IsItemLearned(((Item)sk).ItemID)) { UnityEngineExtensions.ResetLocal(((Component)sk).transform, true); ((EffectSynchronizer)sk).ProcessEffects(); ((CharacterKnowledge)knowledge).AddItem((Item)(object)sk); } if (((CharacterKnowledge)knowledge).IsItemLearned(((Item)sk).ItemID)) { Plugin.Log.LogMessage((object)($"[SPAWN] force-learned skill {((Item)sk).ItemID} '{((Item)sk).Name}' on '{((Object)((Component)ch).gameObject).name}' " + "(BUG-CASTERNOSKILL — AIEUseSkill is a silent no-op on an unlearned skill; the clone's UpdateParentChange sync gate skipped RegisterKnowledge, the caster sibling of Bug 39).")); return true; } Plugin.Log.LogWarning((object)($"[SPAWN] skill {((Item)sk).ItemID} '{((Item)sk).Name}' on '{((Object)((Component)ch).gameObject).name}' REFUSED to learn " + "even after ForceUpdateParentChange + direct AddItem — this caster will not attack (BUG-CASTERNOSKILL).")); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SPAWN] force-learn of skill '" + ((Object)sk).name + "' threw: " + ex.Message)); } return false; } } internal static class CorpseGC { internal static void ScheduleCorpseRemoval(SpawnHandle handle, GameObject corpse) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 if ((int)handle.CorpsePolicy == 1 && !((Object)(object)corpse == (Object)null) && !((Object)(object)Plugin.Instance == (Object)null)) { ((MonoBehaviour)Plugin.Instance).StartCoroutine(RemoveCorpse(corpse, handle.SpeciesKey, handle.OwnerTag, handle.CorpseLingerSeconds, handle.Uid)); } } private static IEnumerator RemoveCorpse(GameObject corpse, string species, string owner, float linger, string uid) { if (linger > 0f) { yield return (object)new WaitForSeconds(linger); } else { yield return null; } if (!((Object)(object)corpse == (Object)null)) { Object.Destroy((Object)(object)corpse); Plugin.Log.LogMessage((object)$"[SPAWN] corpse removed (NoBody): species='{species}' owner='{owner}' linger={linger:0.#}s."); SpawnNet.SendGone(uid, (GoneKind)2); } } } [HarmonyPatch(/*Could not decode attribute arguments.*/)] internal static class CursorControl { internal static bool MenuOpen; internal static bool VanillaMenuFocused; private static CursorStrategy _engaged; private static CharacterUI OwnerUI { get { Character val = Lifecycle.FirstLocalCharacterOrNull(); if (!((Object)(object)val != (Object)null)) { return null; } return val.CharacterUI; } } private static void Postfix(CharacterUI __instance, ref bool __result) { if (__instance == OwnerUI) { VanillaMenuFocused = __result; if (MenuCursorPolicy.EngageSeam(MenuOpen, __result)) { __result = true; } } } internal static void SetMenuOpen(bool open) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Invalid comparison between Unknown and I4 //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Invalid comparison between Unknown and I4 MenuOpen = open; CursorStrategy val = MenuCursorPolicy.ResolveLog(open, VanillaMenuFocused); if (val == _engaged) { return; } _engaged = val; if ((int)val != 1) { if ((int)val == 2) { Plugin.Log.LogMessage((object)"[MENU] cursor: vanilla menu owns cursor (yielding — our window force-closes)."); } else { Plugin.Log.LogMessage((object)"[MENU] cursor: game-seam restored (menu closed — cursor + camera/movement back under game control)."); } } else { Plugin.Log.LogMessage((object)"[MENU] cursor: game-seam released (IsMenuFocused override engaged — game frees cursor + stops camera/movement)."); } } } internal static class EnemySpawner { private static readonly List<SpawnHandle> _spawns = new List<SpawnHandle>(); internal static readonly ViewLease Lease = new ViewLease("[SPAWN]"); private const byte CorpseMuteGroup = 253; private static GameObject _mintHolder; private const float ColdDegradedLogSeconds = 60f; private static readonly Dictionary<string, float> _coldDegradedLoggedAt = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary<string, float> _gateNotEvaluatedLoggedAt = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase); private static readonly List<SpawnHandle> _watchScratch = new List<SpawnHandle>(); private static bool _watchScratchBusy; internal static int PendingReleaseCount => Lease.PendingCount; internal static int TrackedCount => _spawns.Count; private static void ReleaseViewId(int viewId) { Lease.Release(viewId); } private static void DeferViewRelease(int viewId, GameObject body, bool corpseMayPersist = false) { Lease.DeferRelease(viewId, body, corpseMayPersist); } private static void SweepPendingReleases() { Lease.SweepPending((Action<LeaseEntry<GameObject>, LeaseVerdict, float>)AgedOutNotice, (Action<Exception>)CorpseMuteReassertFailed); } private static void AgedOutNotice(LeaseEntry<GameObject> pr, LeaseVerdict verdict, float age) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 if ((int)verdict == 3) { Plugin.Log.LogMessage((object)($"[SPAWN] viewID {pr.ViewId} parked on corpse '{((Object)pr.Body).name}' for " + $"{age:0}s — CORRECT under the Vanilla corpse policy (no corpse GC exists; " + "the id stays reserved until scene unload destroys the corpse and the sweep releases it).")); } else { Plugin.Log.LogWarning((object)($"[SPAWN] viewID {pr.ViewId} has been parked on '{((Object)pr.Body).name}' for " + $"{age:0}s (> {300f:0}s) and the body STILL holds the view — the corpse/scene " + "teardown that should free it never fired. Leaving it reserved (a live-view release would fire PUN's warning); this is a viewID leak — investigate. (skcoopdump/dump shows parked entries + ages.)")); } } private static void CorpseMuteReassertFailed(Exception e) { Plugin.Log.LogWarning((object)($"[SPAWN] re-asserting the corpse send-block (group {(byte)253}) threw " + "(" + e.GetType().Name + ": " + e.Message + ") — muted corpses may resume streaming at guests (V-PARKLEAK). Warned once per session.")); } private static void MuteCorpseView(Character corpse) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003c: 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_0060: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)corpse == (Object)null)) { GameObject gameObject; try { gameObject = ((Component)corpse).gameObject; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SPAWN] corpse view mute failed: " + ex.Message)); return; } MuteResult val = Lease.MuteView(gameObject); if (val.Error != null) { Plugin.Log.LogWarning((object)("[SPAWN] corpse view mute failed: " + val.Error)); } else if (val.Muted) { Plugin.Log.LogMessage((object)($"[SPAWN] muted corpse viewID {val.ViewId} (group {(byte)253} send-blocked; " + "view stays registered, parked id stays reserved) — RunViewUpdate skips blocked groups, so the " + $"corpse stops streaming at guests who can never hold it (V-PARKLEAK). pt={PhotonNetwork.time:F1}")); } } } internal static string PendingReleasesDump() { return Lease.PendingDump(); } private static GameObject MintHolder() { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown if ((Object)(object)_mintHolder == (Object)null) { _mintHolder = new GameObject("SK_MintHolder"); _mintHolder.SetActive(false); } return _mintHolder; } internal static FailReason Preflight(SpawnHandle handle, SpawnOptions opts) { //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Expected I4, but got Unknown //IL_01bc: Unknown result type (might be due to invalid IL or missing references) //IL_01c1: Unknown result type (might be due to invalid IL or missing references) //IL_01c3: Unknown result type (might be due to invalid IL or missing references) //IL_01c5: Unknown result type (might be due to invalid IL or missing references) //IL_01cb: Invalid comparison between Unknown and I4 //IL_01cd: Unknown result type (might be due to invalid IL or missing references) //IL_01cf: Unknown result type (might be due to invalid IL or missing references) //IL_01d5: Invalid comparison between Unknown and I4 //IL_02c5: Unknown result type (might be due to invalid IL or missing references) //IL_01ea: Unknown result type (might be due to invalid IL or missing references) //IL_030d: Unknown result type (might be due to invalid IL or missing references) //IL_0228: Unknown result type (might be due to invalid IL or missing references) //IL_027d: Unknown result type (might be due to invalid IL or missing references) //IL_03bf: Unknown result type (might be due to invalid IL or missing references) //IL_03c1: Unknown result type (might be due to invalid IL or missing references) //IL_03c8: Unknown result type (might be due to invalid IL or missing references) //IL_0354: Unknown result type (might be due to invalid IL or missing references) //IL_038f: Unknown result type (might be due to invalid IL or missing references) opts = opts ?? new SpawnOptions(); if (!((Object)(object)Plugin.Instance == (Object)null) && Plugin.Log != null) { if (!Plugin.Enabled.Value) { Plugin.Log.LogWarning((object)"[SPAWN] refused: [Spawner] Enabled=false (kill-switch)."); return (FailReason)2; } if (PhotonNetwork.isNonMasterClientInRoom) { Plugin.Log.LogWarning((object)"[SPAWN] refused: only the master client spawns (clients receive, Phase 3)."); return (FailReason)3; } Character val = Lifecycle.FirstLocalCharacterOrNull(); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"[SPAWN] refused: no local player character yet."); return (FailReason)4; } int num = 0; try { PhotonPlayer[] otherPlayers = PhotonNetwork.otherPlayers; num = ((otherPlayers != null) ? otherPlayers.Length : 0); } catch { } string names; int num2 = SpawnNet.PeersWithoutHelloCount(out names); RoomSpawnDecision val2 = SpawnPolicy.DecideRoomSpawn(PhotonNetwork.inRoom, num, Plugin.EnableCoopSpawns.Value, num2, Plugin.AllowSpawnInRoom.Value); switch (val2 - 1) { case 4: Notify.Player(val, "Spawn refused: guests are connected and [Coop] EnableCoopSpawns=false ([Spawner] AllowSpawnInRoom=true to override into the ghost behavior)."); Plugin.Log.LogWarning((object)($"[SPAWN] refused (RefuseLegacy): {num} other player(s), coop disabled, " + "AllowSpawnInRoom=false — a broadcast-less spawn is a master-only ghost.")); return (FailReason)11; case 3: Notify.Player(val, $"Spawn refused: {num2} player(s) without a compatible SpawnKit ({names}) — " + "they need the mod, or [Spawner] AllowSpawnInRoom=true accepts they'll see nothing."); Plugin.Log.LogWarning((object)("[SPAWN] refused (PeersNotReady): no sk.hello from " + names + " — unmodded or incompatible (skcoopdump shows the hello ledger).")); return (FailReason)12; case 1: Plugin.Log.LogWarning((object)("[SPAWN] co-op DEGRADED spawn (AllowSpawnInRoom override): " + names + " never handshook — modded peers will mirror, those peers get the pre-Phase-3 ghost.")); break; case 2: Plugin.Log.LogWarning((object)"[SPAWN] ghost spawn (coop disabled + AllowSpawnInRoom override): NO broadcast — guests get warn spam + the invulnerable-replica combat lock. On your head."); break; case 0: Plugin.Log.LogMessage((object)($"[SPAWN] co-op spawn: {num} peer(s) all handshaken — " + "broadcast follows once the spawn resolves Alive.")); break; } if (PhotonNetwork.inRoom && WarmMirror.ParticipatingCount() > 0) { handle.RoomGateEvaluated = true; RoomWarmDecision val3 = Spawner.RoomWarmDecision(handle.SpeciesKey); if ((int)val3.Verdict == 1 || (int)val3.Verdict == 2) { if (!opts.IgnoreRoomWarm) { string text = string.Join(", ", Array.ConvertAll(val3.Actors, (int a) => a.ToString())); Notify.Player(val, $"Spawn refused: '{handle.SpeciesKey}' is cold on {val3.Actors.Length} guest(s) — " + "they would stall loading a donor scene to see it ([Coop] RoomWarmMode)."); Plugin.Log.LogWarning((object)("[SPAWN] refused '" + handle.SpeciesKey + "' — cold on peer actor(s) [" + text + "] (RoomWarmMode=RoomStrict, " + val3.Reason + "). skwarmdump shows each peer's row.")); return (FailReason)13; } Plugin.Log.LogWarning((object)("[SPAWN] '" + handle.SpeciesKey + "' is cold on peer actor(s) [" + string.Join(", ", Array.ConvertAll(val3.Actors, (int a) => a.ToString())) + "] — spawning anyway (SpawnOptions.IgnoreRoomWarm: the dev-verb 'force' escape).")); } else if (val3.Degraded) { if (LogColdDegraded(handle.SpeciesKey)) { Plugin.Log.LogWarning((object)("[SPAWN] '" + handle.SpeciesKey + "' cold on actor(s) [" + string.Join(", ", Array.ConvertAll(val3.Actors, (int a) => a.ToString())) + "] — spawning anyway (RoomWarmMode=RoomDegraded, " + val3.Reason + "); asking them to warm it.")); } Spawner.RequestRoomWarm(new string[1] { handle.SpeciesKey }); } handle.RoomGateOk = (int)val3.Verdict == 0 && !val3.Degraded; } if ((Object)(object)AISquadManager.Instance == (Object)null) { Notify.Player(val, "Cannot spawn here (no AI manager in this scene)."); return (FailReason)5; } WatchTick(); if (_spawns.Count + 1 > Plugin.MaxActiveSpawns.Value) { Notify.Player(val, $"Spawn refused: would exceed MaxActiveSpawns ({_spawns.Count} active/pending, cap {Plugin.MaxActiveSpawns.Value})."); return (FailReason)6; } _spawns.Add(handle); return (FailReason)0; } return (FailReason)1; } private static bool LogColdDegraded(string speciesKey) { float unscaledTime = Time.unscaledTime; if (_coldDegradedLoggedAt.TryGetValue(speciesKey ?? "", out var value) && unscaledTime - value < 60f) { return false; } _coldDegradedLoggedAt[speciesKey ?? ""] = unscaledTime; return true; } private static bool LogGateNotEvaluated(string speciesKey) { float unscaledTime = Time.unscaledTime; if (_gateNotEvaluatedLoggedAt.TryGetValue(speciesKey ?? "", out var value) && unscaledTime - value < 60f) { return false; } _gateNotEvaluatedLoggedAt[speciesKey ?? ""] = unscaledTime; return true; } internal static IEnumerator SpawnRoutine(SpawnHandle handle, SpawnOptions opts) { opts = opts ?? new SpawnOptions(); GameObject template = null; bool acquired = false; yield return SpawnTemplates.Acquire(handle.SpeciesKey, delegate(GameObject t) { template = t; acquired = true; }); while (!acquired) { yield return null; } if (handle.State != SpawnState.Pending) { yield break; } Character val = Lifecycle.FirstLocalCharacterOrNull(); if ((Object)(object)template == (Object)null) { bool flag = ResolvesInDonorTable(handle.SpeciesKey); if ((Object)(object)val != (Object)null) { Notify.Player(val, flag ? ("'" + handle.SpeciesKey + "' is known, but no body could be harvested for it (see log).") : ("No spawnable species matches '" + handle.SpeciesKey + "' (see log / 'spawnlist').")); } Fail(handle, (FailReason)(flag ? 10 : 7)); yield break; } if ((Object)(object)val == (Object)null) { Fail(handle, (FailReason)4); yield break; } if (!Mint(template, val, opts, handle)) { Fail(handle, (FailReason)8); yield break; } handle.MintFrame = Time.frameCount; yield return null; yield return null; if (handle.State != SpawnState.Pending) { yield break; } GameObject val2 = (((Object)(object)handle.Character != (Object)null) ? ((Component)handle.Character).gameObject : null); if ((Object)(object)val2 == (Object)null || !val2.activeInHierarchy) { Plugin.Log.LogError((object)"[SPAWN] spawn went INACTIVE within two frames of activation — the ASYNC duplicate-UID guard signature (AddCharacter runs from Character.Start; grep output_log.txt for 'has the same UID'). Treating as a mint FAILURE: destroying the deactivated clone and releasing its viewID (a zombie would pin a cap slot + leak the id)."); if ((Object)(object)val2 != (Object)null) { Object.Destroy((Object)(object)val2); } DeferViewRelease(handle.ViewId, val2); Fail(handle, (FailReason)8); yield break; } float num = -1f; bool flag2 = false; try { Character character = handle.Character; if ((Object)(object)character != (Object)null && (Object)(object)character.Stats != (Object)null) { string text = "ok"; try { character.Stats.RefreshVitalMaxStat(false); } catch (Exception) { text = "fallback"; try { character.Stats.UpdateStats(); } catch (Exception ex2) { text = "threw"; Plugin.Log.LogWarning((object)("[SPAWN] mint-heal stat refresh threw: " + ex2.Message)); } } float currentHealth = character.Stats.CurrentHealth; float maxHealth = character.Stats.MaxHealth; float baseMaxHealth = character.Stats.BaseMaxHealth; float num2 = MintHeal.Initial(currentHealth, maxHealth); if (num2 >= 0f) { character.Stats.SetHealth(num2); } int num3 = 1; try { if ((Object)(object)Global.Lobby != (Object)null) { num3 = Global.Lobby.PlayersInLobby.Count; } } catch { } flag2 = num3 > 1; num = maxHealth; Plugin.Log.LogMessage((object)($"[SPAWN] mint-heal uid={handle.Uid} refresh={text} hp={currentHealth:0.#}/{maxHealth:0.#} base={baseMaxHealth:0.#} " + $"-> {character.Stats.CurrentHealth:0.#}/{character.Stats.MaxHealth:0.#} lobby={num3} master={PhotonNetwork.isMasterClient} " + $"frame+{Time.frameCount - handle.MintFrame}")); if (num2 >= 0f) { Plugin.Log.LogMessage((object)($"[SPAWN] healed to full at mint: {currentHealth:0.#} -> {num2:0.#} " + "(the clone carries the donor's serialized current health).")); } } } catch (Exception ex3) { Plugin.Log.LogWarning((object)("[SPAWN] heal-to-full at mint threw: " + ex3.Message)); } if (flag2 && num >= 0f && (Object)(object)Plugin.Instance != (Object)null) { ((MonoBehaviour)Plugin.Instance).StartCoroutine(SettleHealth(handle, num)); } HumanoidWeapon.EnsureEquipped(handle.Character); CasterSkills.EnsureLearned(handle.Character); if (Plugin.PostActivationVisualPass != null && Plugin.PostActivationVisualPass.Value) { VisualPass.Run(handle.Character, handle.SpeciesKey); } if (PhotonNetwork.inRoom && !opts.IgnoreRoomWarm && WarmMirror.ParticipatingCount() > 0) { RoomWarmDecision val3 = Spawner.RoomWarmDecision(handle.SpeciesKey); if ((int)val3.Verdict == 1 || (int)val3.Verdict == 2) { if (handle.RoomGateEvaluated) { string text2 = string.Join(", ", Array.ConvertAll(val3.Actors, (int a) => a.ToString())); Plugin.Log.LogWarning((object)("[SPAWN] refused '" + handle.SpeciesKey + "' — cold on peer actor(s) [" + text2 + "] (RoomWarmMode=RoomStrict, " + val3.Reason + ") — the room went cold DURING the mint (an LRU eviction, a fresh joiner, or a spent budget). Tearing the body back down.")); GameObject val4 = (((Object)(object)handle.Character != (Object)null) ? ((Component)handle.Character).gameObject : null); if ((Object)(object)val4 != (Object)null) { Object.Destroy((Object)(object)val4); } DeferViewRelease(handle.ViewId, val4); Fail(handle, (FailReason)13); Spawner.RequestRoomWarm(new string[1] { handle.SpeciesKey }); yield break; } if (LogGateNotEvaluated(handle.SpeciesKey)) { Plugin.Log.LogWarning((object)("[SPAWN] '" + handle.SpeciesKey + "' — a peer joined during the mint; not enforcing the room gate on a spawn authorised before it (degraded)")); } Spawner.RequestRoomWarm(new string[1] { handle.SpeciesKey }); } else { handle.RoomGateOk = (int)val3.Verdict == 0 && !val3.Degraded; } } handle.Resolve(SpawnState.Alive, (FailReason)0); SpawnNet.BroadcastSpawn(handle); } private static void Fail(SpawnHandle handle, FailReason reason) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) _spawns.Remove(handle); handle.Resolve(SpawnState.Failed, reason); } internal static SpawnHandle FindByUid(string uid) { if (string.IsNullOrEmpty(uid)) { return null; } foreach (SpawnHandle spawn in _spawns) { if (string.Equals(spawn.Uid, uid, StringComparison.Ordinal)) { return spawn; } } return null; } private static bool ResolvesInDonorTable(string speciesKey) { try { string text = default(string); List<string> list = default(List<string>); return SpeciesTable.TryResolveKey<List<string>>(DonorHarvest.DonorScenes, speciesKey?.Trim() ?? "", ref text, ref list, (string)null) && list != null && list.Count > 0; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SPAWN] donor-table resolve for '" + speciesKey + "' threw: " + ex.Message + " — treating as unknown species.")); return false; } } internal static bool RefusedNonMasterHarvest(string tag, bool allowVoluntary = false) { if (!PhotonNetwork.isNonMasterClientInRoom) { return false; } if (allowVoluntary && Plugin.GuestPrewarm != null && Plugin.GuestPrewarm.Value) { return false; } ModLog log = Plugin.Log; if (log != null) { log.LogWarning((object)(tag + " refused: only the master client harvests donor scenes — a client would load one locally (hitch + LightProbes decay, invisible to the host)." + (allowVoluntary ? " [Coop] GuestPrewarm=false; set it true to queue a voluntary warm instead." : " Phase 3."))); } return true; } private static IEnumerator SettleHealth(SpawnHandle handle, float prevMax) { SettleState st = SettleState.Start(prevMax); string uid = handle.Uid; Character character; float maxHealth; do { yield return (object)new WaitForSeconds(0.1f); if (!handle.IsAlive) { yield break; } character = handle.Character; if ((Object)(object)character == (Object)null || (Object)(object)character.Stats == (Object)null) { yield break; } try { float currentHealth = character.Stats.CurrentHealth; maxHealth = character.Stats.MaxHealth; float prevMax2 = st.PrevMax; float num = MintHeal.Step(ref st, currentHealth, maxHealth); if (num >= 0f) { character.Stats.SetHealth(num); Plugin.Log.LogMessage((object)$"[SPAWN] mint-heal settle uid={uid} max {prevMax2:0.#} -> {maxHealth:0.#} hp {currentHealth:0.#} -> {num:0.#} t={st.Elapsed:0.0}s"); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SPAWN] mint-heal settle uid=" + uid + " threw: " + ex.Message)); yield break; } } while (!MintHeal.Done(ref st)); bool isStable = ((SettleState)(ref st)).IsStable; float num2 = -1f; try { num2 = character.Stats.BaseMaxHealth; } catch { } string text = $"[SPAWN] mint-heal settle uid={uid} done hp={character.Stats.CurrentHealth:0.#}/{maxHealth:0.#} base={num2:0.#} " + string.Format("(startHp={0:0.#} startMax={1:0.#} moved={2}) ticks={3} t={4:0.0}s stable={5}", st.StartHp, st.StartMax, st.SawChange ? "T" : "F", st.Ticks, st.Elapsed, isStable ? "T" : "F"); if (!isStable) { Plugin.Log.LogWarning((object)(text + " — budget hit before the max stat held still; check spawndump hp=cur/max")); } else if (!st.SawChange && maxHealth == num2) { Plugin.Log.LogWarning((object)(text + " — max==base: coop stack never landed within budget")); } else { Plugin.Log.LogMessage((object)text); } } private static bool Mint(GameObject template, Character player, SpawnOptions opts, SpawnHandle handle) { //IL_040a: 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_0082: 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_00be: Unknown result type (might be due to invalid IL or missing references) //IL_0485: Unknown result type (might be due to invalid IL or missing references) //IL_0490: Unknown result type (might be due to invalid IL or missing references) //IL_0496: Invalid comparison between Unknown and I4 //IL_016e: Unknown result type (might be due to invalid IL or missing references) //IL_0173: Unknown result type (might be due to invalid IL or missing references) //IL_0146: Unknown result type (might be due to invalid IL or missing references) //IL_014b: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_0180: Unknown result type (might be due to invalid IL or missing references) //IL_0182: Unknown result type (might be due to invalid IL or missing references) //IL_0187: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_012e: 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_01c4: Unknown result type (might be due to invalid IL or missing references) //IL_01cc: Unknown result type (might be due to invalid IL or missing references) //IL_01ce: Unknown result type (might be due to invalid IL or missing references) //IL_01b4: Unknown result type (might be due to invalid IL or missing references) //IL_01b6: Unknown result type (might be due to invalid IL or missing references) //IL_01ad: Unknown result type (might be due to invalid IL or missing references) GameObject val = Object.Instantiate<GameObject>(template, MintHolder().transform); int viewId = -1; try { ((Object)val).name = ((Object)template).name.Replace("SK_Template_", "SK_Spawn_"); Character component = val.GetComponent<Character>(); if ((Object)(object)component == (Object)null) { Plugin.Log.LogError((object)"[SPAWN] template clone has no Character component — destroying."); Object.Destroy((Object)(object)val); return false; } MintNormalize.Apply(val, component, opts, out var uid, ref viewId); handle.CorpsePolicy = CorpseRules.EffectivePolicy(opts.Corpse, Plugin.DefaultCorpsePolicy.Value); handle.CorpseLingerSeconds = CorpseRules.EffectiveLinger(opts.CorpseLingerSeconds, Plugin.DefaultCorpseLingerSeconds.Value); handle.CoopFaction = ((!opts.Faction.HasValue) ? (-1) : ((int)opts.Faction.Value)); handle.CoopStripQuestEvents = opts.StripQuestEvents; handle.ConsumerData = opts.ConsumerData ?? ""; if (Plugin.AiDisableDistance != null && Plugin.AiDisableDistance.Value > 0f) { CharacterAI component2 = val.GetComponent<CharacterAI>(); if ((Object)(object)component2 != (Object)null) { component2.DistanceToUpdate = new Vector2(Plugin.AiDisableDistance.Value, Plugin.AiDisableDistance.Value); } } Vector3 val2; string note; if (opts.Position.HasValue) { val2 = opts.Position.Value; note = "pos=explicit"; } else { val2 = PickGround(player, SpawnPolicy.EffectiveDistance(opts.Distance, Plugin.SpawnDistance.Value), out note); } Vector3 val3 = Flat(((Component)player).transform.position - val2); Quaternion val4 = (Quaternion)(((??)opts.Rotation) ?? ((((Vector3)(ref val3)).sqrMagnitude > 0.01f) ? Quaternion.LookRotation(val3) : Quaternion.identity)); val.transform.SetPositionAndRotation(val2, val4); if (opts.OnBeforeActivate != null) { try { opts.OnBeforeActivate.Invoke(val, component); } catch (Exception arg) { Plugin.Log.LogError((object)("[SPAWN] consumer OnBeforeActivate threw for '" + handle.SpeciesKey + "' " + $"(owner '{handle.OwnerTag}') — hook skipped, mint continues: {arg}")); } } val.transform.SetParent((Transform)null, true); MintNormalize.DeferAiCulling(val, "'" + ((Object)val).name + "' (master mint)"); CharacterAI component3 = val.GetComponent<CharacterAI>(); if ((Object)(object)component3 != (Object)null) { try { component3.InitStartPos(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SPAWN] InitStartPos threw for '" + ((Object)val).name + "' (" + ex.GetType().Name + ": " + ex.Message + ") — the AI's wander home was NOT anchored to the spawn position.")); } } string arg2 = "?"; string arg3 = "?"; string text = "?"; string text2 = "?"; string text3 = "?"; try { arg2 = (((Object)(object)component3 != (Object)null && component3.AiStates != null) ? component3.AiStates.Length : 0).ToString(); } catch { } try { arg3 = (((Object)(object)component3 != (Object)null && (Object)(object)component3.CurrentAiState != (Object)null) ? ((object)component3.CurrentAiState).GetType().Name : "none"); } catch { } try { text = (((Object)(object)component.Stats != (Object)null) ? $"{component.Stats.CurrentHealth:0.#}/{component.Stats.MaxHealth:0.#} base={component.Stats.BaseMaxHealth:0.#}" : "?"); } catch { } try { text2 = ((object)Unsafe.As<Factions, Factions>(ref component.Faction)/*cast due to .constrained prefix*/).ToString(); } catch { } try { text3 = component.Alive.ToString(); } catch { } Plugin.Log.LogMessage((object)($"[SPAWN] '{((Object)val).name}' uid={uid} viewID={viewId} owner='{handle.OwnerTag}' pos={val2:F1} ({note}) | " + string.Format("active={0} alive={1} hp={2} faction={3} lifetime={4} ", val.activeInHierarchy, text3, text, text2, opts.LifetimeSeconds.HasValue ? opts.LifetimeSeconds.Value.ToString("0.#") : "donor") + string.Format("corpse={0}{1} | ", handle.CorpsePolicy, ((int)handle.CorpsePolicy == 1) ? $"({handle.CorpseLingerSeconds:0.#}s)" : "") + $"aiStates={arg2} state={arg3} agent={(Object)(object)val.GetComponent<NavMeshAgent>() != (Object)null} " + $"charAIDisable={(Object)(object)val.GetComponent<CharAIDisable>() != (Object)null} " + "aiLive=" + DescribeAiLiveness(component, component3))); try { string text4 = SpawnTemplates.ResolvedDonorName(handle.SpeciesKey); if (!string.IsNullOrEmpty(text4) && !string.Equals(text4, handle.SpeciesKey, StringComparison.Ordinal)) { Plugin.Log.LogMessage((object)("[SPAWN] donor for '" + handle.SpeciesKey + "': '" + text4 + "' (template identity — see the [SPAWN] label warning if mismatched).")); } } catch { } if (!val.activeInHierarchy) { Plugin.Log.LogError((object)"[SPAWN] spawn is INACTIVE right after activation — duplicate-UID guard signature (grep output_log.txt for 'has the same UID'). Treating as a mint FAILURE: destroying the deactivated clone and releasing its viewID (a zombie would pin a cap slot + leak the id)."); Object.Destroy((Object)(object)val); DeferViewRelease(viewId, val); return false; } handle.Character = component; handle.Uid = uid; handle.ViewId = viewId; return true; } catch (Exception arg4) { Plugin.Log.LogError((object)$"[SPAWN] mint threw — destroying pending clone and releasing viewID {viewId}: {arg4}"); if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); DeferViewRelease(viewId, val); } else { ReleaseViewId(viewId); } return false; } } private static Vector3 PickGround(Character player, float distance, out string note) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) Vector3 position = ((Component)player).transform.position; Vector3 forward = ((Component)player).transform.forward; IReadOnlyList<(float, float)> readOnlyList = RingPlacement.Candidates(forward.x, forward.z, distance, 16); Vector3 val = default(Vector3); NavMeshHit val2 = default(NavMeshHit); for (int i = 0; i < readOnlyList.Count; i++) { ((Vector3)(ref val))..ctor(position.x + readOnlyList[i].Item1, position.y, position.z + readOnlyList[i].Item2); if (NavMesh.SamplePosition(val, ref val2, 4f, -1) && Mathf.Abs(((NavMeshHit)(ref val2)).position.y - position.y) <= 2.5f) { note = $"ring[{i}]"; return ((NavMeshHit)(ref val2)).position; } } note = "NO navmesh candidate in elevation band — placed ahead unprobed"; Vector3 val3 = Flat(forward); Vector3 val4; if (!(((Vector3)(ref val3)).sqrMagnitude > 0.01f)) { val4 = Vector3.forward; } else { val3 = Flat(forward); val4 = ((Vector3)(ref val3)).normalized; } return position + val4 * distance; } private static Vector3 Flat(Vector3 v) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) return new Vector3(v.x, 0f, v.z); } internal static void Despawn(SpawnHandle handle, bool kill) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) if (handle == null) { return; } if (handle.State == SpawnState.Pending) { Fail(handle, (FailReason)9); } else { if (handle.State != SpawnState.Alive) { return; } Character character = handle.Character; if (kill && (Object)(object)character != (Object)null && SafeAlive(character)) { try { character.ReceiveHit((Weapon)null, 999999f, Vector3.forward, character.CenterPosition, 45f, 1f, (Character)null, 0f); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SPAWN] overkill ReceiveHit threw on '" + ((Object)((Component)character).gameObject).name + "' (uid " + handle.Uid + ") — a third-party ReceiveHit patch? Falling back to a silent despawn. (" + ex.Message + ")")); } if (!SafeAlive(character)) { Plugin.Log.LogMessage((object)("[SPAWN] overkilled '" + ((Object)((Component)character).gameObject).name + "' (uid " + handle.Uid + ") — WatchTick will resolve the death (loot + disengage on the Died transition).")); return; } Plugin.Log.LogWarning((object)("[SPAWN] overkill left '" + ((Object)((Component)character).gameObject).name + "' Alive (invincible/resistant?) — falling back to a silent despawn (uid " + handle.Uid + ").")); } if (_spawns.Remove(handle)) { SpawnDisengage.DisengageSpawn((RemovalReason)0, character); GameObject val = (((Object)(object)character != (Object)null) ? ((Component)character).gameObject : null); if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } DeferViewRelease(handle.ViewId, val); handle.State = SpawnState.Despawned; SpawnNet.SendGone(handle.Uid, (GoneKind)1); handle.FireDespawned(); } } } public static void DespawnAll(string ownerTag, bool kill) { WatchTick(); int num = 0; int num2 = 0; SpawnHandle[] array = _spawns.ToArray(); foreach (SpawnHandle spawnHandle in array) { if (SpawnPolicy.MatchesOwner(spawnHandle.OwnerTag, ownerTag)) { num++; try { Despawn(spawnHandle, kill); } catch (Exception ex) { num2++; Plugin.Log.LogWarning((object)("[SPAWN] despawnall: Despawn threw for uid " + spawnHandle.Uid + " — continuing the sweep. (" + ex.Message + ")")); } } } Plugin.Log.LogMessage((object)("[SPAWN] despawnall (" + (kill ? "kill" : "silent") + ((ownerTag != null) ? (", owner '" + ownerTag + "'") : "") + $"): {num} processed" + ((num2 > 0) ? $" ({num2} threw — see warnings)" : "") + $", {_spawns.Count} still tracked pre-destroy (async — spawndump for the settled count).")); } internal static IReadOnlyList<SpawnHandle> Snapshot(string ownerTag) { List<SpawnHandle> list = new List<SpawnHandle>(); Snapshot(ownerTag, list); return list; } internal static void Snapshot(string ownerTag, List<SpawnHandle> into) { into.Clear(); foreach (SpawnHandle spawn in _spawns) { if (SpawnPolicy.MatchesOwner(spawn.OwnerTag, ownerTag)) { into.Add(spawn); } } } internal static int LootProbeAll() { WatchTick(); int num = 0; SpawnHandle[] array = _spawns.ToArray(); foreach (SpawnHandle spawnHandle in array) { if (spawnHandle.State == SpawnState.Alive) { Character character = spawnHandle.Character; if (!((Object)(object)character == (Object)null)) { LootProbe.Log(character, "probe '" + spawnHandle.SpeciesKey + "' (owner '" + spawnHandle.OwnerTag + "')"); num++; } } } return num; } public static string Dump() { //IL_024d: Unknown result type (might be due to invalid IL or missing references) //IL_0259: Unknown result type (might be due to invalid IL or missing references) //IL_02c4: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) WatchTick(); Character val = Lifecycle.FirstLocalCharacterOrNull(); string arg = ((!PhotonNetwork.inRoom) ? "" : (" role=" + (PhotonNetwork.isMasterClient ? "MASTER" : "GUEST") + " (replicas: see [MIRROR] below)")); int pendingPersistCount = Lease.PendingPersistCount; int num = PendingReleaseCount - pendingPersistCount; string text = ((pendingPersistCount > 0) ? $" ({pendingPersistCount} viewID release(s) parked on surviving corpses)" : "") + ((num > 0) ? $" ({num} viewID release(s) parked pending teardown)" : ""); object arg2 = _spawns.Count; Scene activeScene = SceneManager.GetActiveScene(); StringBuilder stringBuilder = new StringBuilder($"[SPAWN] {arg2} tracked spawn(s) in '{((Scene)(ref activeScene)).name}'{arg}" + text + ":"); foreach (SpawnHandle spawn in _spawns) { if (spawn.State == SpawnState.Pending) { stringBuilder.Append("\n '" + spawn.SpeciesKey + "' owner='" + spawn.OwnerTag + "' state=Pending (template acquiring)"); continue; } Character character = spawn.Character; if ((Object)(object)character == (Object)null) { continue; } string text2 = "?"; string text3 = "?"; string text4 = "-"; try { text2 = (((Object)(object)character.Stats != (Object)null) ? $"{character.Stats.CurrentHealth:0.#}/{character.Stats.MaxHealth:0.#} base={character.Stats.BaseMaxHealth:0.#}" : "?"); } catch { } CharacterAI component = ((Component)character).GetComponent<CharacterAI>(); try { text3 = (((Object)(object)component != (Object)null && (Object)(object)component.CurrentAiState != (Object)null) ? ((object)component.CurrentAiState).GetType().Name : "none"); } catch { } try { Character val2 = (((Object)(object)component != (Object)null && (Object)(object)component.TargetingSystem != (Object)null) ? component.TargetingSystem.LockedCharacter : null); if ((Object)(object)val2 != (Object)null) { text4 = val2.Name; } } catch { } float num2 = (((Object)(object)val != (Object)null) ? Vector3.Distance(((Component)val).transform.position, ((Component)character).transform.position) : (-1f)); stringBuilder.Append($"\n '{((Object)((Component)character).gameObject).name}' species='{spawn.SpeciesKey}' owner='{spawn.OwnerTag}' lifecycle={spawn.State} " + $"uid={character.UID} viewID={spawn.ViewId} alive={SafeAlive(character)} hp={text2} dist={num2:0.#} " + $"active={((Component)character).gameObject.activeInHierarchy} aiEnabled={(Object)(object)component != (Object)null && ((Behaviour)component).enabled} " + "aiLive=" + DescribeAiLiveness(character, component) + " state=" + text3 + " target=" + text4 + " " + HumanoidWeapon.StateFor(character) + " " + CasterSkills.StateFor(character)); } string text5 = PendingReleasesDump(); if (text5.Length > 0) { stringBuilder.Append('\n').Append(text5); } return stringBuilder.ToString(); } internal static string DescribeAiLiveness(Character c, CharacterAI cai) { //IL_0096: Unknown result type (might be due to invalid IL or missing references) try { bool flag = (Object)(object)cai != (Object)null; int num = 0; try { num = ((flag && cai.AiStates != null) ? cai.AiStates.Length : 0); } catch { } bool flag2 = false; try { flag2 = flag && ((Behaviour)cai).enabled; } catch { } bool flag3 = false; try { flag3 = (Object)(object)c != (Object)null && c.IsStartInitDone; } catch { } bool flag4 = false; try { flag4 = flag && ((CharacterControl)cai).CloseToPlayer; } catch { } bool flag5 = false; try { flag5 = (Object)(object)NetworkLevelLoader.Instance != (Object)null && NetworkLevelLoader.Instance.IsGameplayPaused; } catch { } return AiLivenessRules.Format(AiLivenessRules.Classify(flag, num, flag2, flag3, flag4, flag5)); } catch (Exception ex) { return "?(" + ex.GetType().Name + ")"; } } public static void WatchTick() { SweepPendingReleases(); bool flag = !_watchScratchBusy; List<SpawnHandle> list; if (flag) { _watchScratchBusy = true; _watchScratch.Clear(); list = _watchScratch; } else { list = new List<SpawnHandle>(_spawns.Count); } list.AddRange(_spawns); try { WatchWalk(list); } finally { if (flag) { _watchScratch.Clear(); _watchScratchBusy = false; } } } private static void WatchWalk(List<SpawnHandle> snapshot) { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Expected I4, but got Unknown //IL_01ac: Unknown result type (might be due to invalid IL or missing references) //IL_01b2: Invalid comparison between Unknown and I4 //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Unknown result type (might be due to invalid IL or missing references) foreach (SpawnHandle item in snapshot) { if (item.State != SpawnState.Alive) { continue; } bool flag = (Object)(object)item.Character != (Object)null; bool flag2 = flag && SafeAlive(item.Character); SpawnState spawnState = (SpawnState)SpawnWatch.Next((WatchState)item.State, flag, flag2); if (spawnState == item.State) { continue; } GameObject val = ((spawnState == SpawnState.Died) ? ((Component)item.Character).gameObject : null); if (spawnState == SpawnState.Died) { LootProbe.Log(item.Character, "death '" + item.SpeciesKey + "'"); } SpawnDisengage.DisengageSpawn((RemovalReason)((spawnState == SpawnState.Died) ? 2 : 3), (spawnState == SpawnState.Died) ? item.Character : null); if (!_spawns.Remove(item)) { continue; } bool flag3 = false; if (spawnState == SpawnState.Died && (int)item.CorpsePolicy == 0 && Plugin.CorpseViewRelease != null && Plugin.CorpseViewRelease.Value) { try { Views.Neutralize(val, "corpse '" + item.SpeciesKey + "' uid " + item.Uid); flag3 = true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SPAWN] CorpseViewRelease neutralize threw for uid " + item.Uid + " (" + ex.GetType().Name + ": " + ex.Message + ") — falling back to the mute-and-park path.")); } if (flag3) { ReleaseViewId(item.ViewId); } } if (!flag3) { if (spawnState == SpawnState.Died) { MuteCorpseView(item.Character); } DeferViewRelease(item.ViewId, (spawnState == SpawnState.Died) ? val : null, spawnState == SpawnState.Died && (int)item.CorpsePolicy == 0); } item.State = spawnState; Plugin.Log.LogMessage((object)$"[WATCH] '{item.SpeciesKey}' (owner '{item.OwnerTag}', uid {item.Uid}) -> {spawnState}."); SpawnNet.SendGone(item.Uid, (GoneKind)(spawnState != SpawnState.Died)); if (spawnState == SpawnState.Died) { item.FireDied(); } else { item.FireDespawned(); } if (spawnState == SpawnState.Died) { CorpseGC.ScheduleCorpseRemoval(item, val); } } } internal static bool SafeAlive(Character c) { try { return c.Alive; } catch { return false; } } } internal static class ExpeditionRun { public static bool InProgress { get; private set; } public static string CurrentSpecies { get; private set; } public static string ForceReset() { if (!InProgress) { return "[EXPEDITION] SpawnKit's trip guard is already open — nothing to reset on this side."; } string text = CurrentSpecies ?? "?"; InProgress = false; CurrentSpecies = null; return "[EXPEDITION] FORCE RESET SpawnKit's trip guard (was waiting on '" + text + "'). The spawn menu accepts expeditions again."; } public static bool ForSpecies(string speciesKey, Action<bool, string> onDone, bool force = false) { string key = (speciesKey ?? "").Trim(); bool fired = false; Action<bool, string> done = delegate(bool ok, string why) { if (fired) { Plugin.Log.LogWarning((object)("[EXPEDITION] double completion for '" + key + "' suppressed (" + why + ").")); return; } fired = true; try { onDone?.Invoke(ok, why); } catch (Exception arg2) { Plugin.Log.LogError((object)$"[EXPEDITION] onDone callback for '{key}' threw: {arg2}"); } }; if (!Plugin.EnableExpeditions.Value) { done.Invoke(false, "expeditions are disabled ([Expedition] EnableExpeditions = false)"); return false; } if (key.Length == 0) { done.Invoke(false, "no species given"); return false; } if (InProgress || ExpeditionHarvest.InProgress) { done.Invoke(false, "an expedition is already running"); return false; } if (!force && Spawner.CanMintNow(key)) { done.Invoke(true, "already resident — no trip needed"); return false; } string text = default(string); List<string> list = default(List<string>); if (!SpeciesTable.TryResolveKey<List<string>>(DonorHarvest.DonorScenes, key, ref text, ref list, (string)null) || list == null || list.Count == 0) { done.Invoke(false, "'" + key + "' is not in the donor table — 'spawnlist' shows the spawnable species"); return false; } if (!force && !Spawner.IsExpeditionOnly(key)) { done.Invoke(false, "'" + key + "' has an ADDITIVE donor — 'spawnprewarm " + key + "' harvests it with no loading screens (pass 'force' to take the trip anyway)"); return false; } List<string> list2 = default(List<string>); string text2 = default(string); if (!DonorHarvest.TryGetExpeditionScenes(key, ref list2, ref text2) || list2 == null || list2.Count == 0) { done.Invoke(false, "'" + key + "' has no expedition donor scene (see 'spawnlist' / DonorScenes.txt)"); return false; } string scene = list2[0]; InProgress = true; CurrentSpecies = key; try { if (!ExpeditionOrchestrator.BeginTrip(scene, (Action<TripResult>)delegate(TripResult trip) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) InProgress = false; CurrentSpecies = null; if (!trip.EndedHome) { Plugin.Log.LogError((object)("[EXPEDITION] SpawnKit: the trip to '" + scene + "' did NOT bring the party home — refusing to spawn on top of it. See the teardown line above; 'goto <scene>' recovers.")); done.Invoke(false, "the expedition did not bring you home — see the log"); } else if (!trip.PayloadRan) { done.Invoke(false, "the expedition to '" + scene + "' was aborted before it could harvest"); } else { Plugin.Log.LogMessage((object)$"[EXPEDITION] SpawnKit: home from '{scene}' — {trip.Built} new body template(s) cached."); done.Invoke(true, (trip.Built > 0) ? $"harvested {trip.Built} new body template(s) from {scene}" : (scene + " harvested (0 new — the cache already covered it)")); } })) { InProgress = false; CurrentSpecies = null; done.Invoke(false, "the expedition would not start — see the refusal in the log"); return false; } if (!fired) { Plugin.Log.LogMessage((object)("[EXPEDITION] SpawnKit: '" + text2 + "' → donor region '" + scene + "'" + ((list2.Count > 1) ? $" (+{list2.Count - 1} more candidate(s))" : "") + " — two loading screens, there and back. One trip caches EVERY species that region donates.")); } return true; } catch (Exception arg) { InProgress = false; CurrentSpecies = null; Plugin.Log.LogError((object)$"[EXPEDITION] SpawnKit: launching the trip to '{scene}' threw: {arg}"); done.Invoke(false, "the expedition threw before it could start — see the log"); return false; } } } internal static class GhostDiag { private static int sampling; internal static bool Enabled { get { if (Plugin.GhostDiagnostics != null) { return Plugin.GhostDiagnostics.Value; } return false; } } internal static void Dump(Character ch, string tag) { if ((Object)(object)ch == (Object)null || (Object)(object)((Component)ch).gameObject == (Object)null) { return; } try { Plugin.Log.LogMessage((object)Describe(ch, tag)); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[GHOSTDIAG] " + tag + " threw: " + ex.Message)); } } internal static void DumpDelayed(Character ch) { if (!((Object)(object)ch == (Object)null) && Enabled && !((Object)(object)Plugin.Instance == (Object)null) && sampling < 3) { sampling++; ((MonoBehaviour)Plugin.Instance).StartCoroutine(DelayedRoutine(ch)); } } private static IEnumerator DelayedRoutine(Character ch) { try { for (int f = 0; f < 60; f++) { if ((Object)(object)ch == (Object)null || (Object)(object)((Component)ch).gameObject == (Object)null) { yield break; } yield return null; } float until = Time.unscaledTime + 5f; while (Time.unscaledTime < until && IsGameplayLoading()) { yield return null; } if (!((Object)(object)ch == (Object)null) && !((Object)(object)((Component)ch).gameObject == (Object)null)) { Dump(ch, "delayed"); } } finally { sampling--; } } private static bool IsGameplayLoading() { try { return (Object)(object)NetworkLevelLoader.Instance != (Object)null && NetworkLevelLoader.Instance.IsGameplayLoading; } catch { return false; } } internal static void DumpNearby(string nameFilter, bool fix, float radius = 30f) { //IL_007c: 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) Character val = Lifecycle.FirstLocalCharacterOrNull(); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"[GHOSTDIAG] no local player — nothing to census."); return; } CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null) { Plugin.Log.LogWarning((object)"[GHOSTDIAG] no CharacterManager."); return; } DictionaryExt<string, Character> characters = instance.Characters; int num = 0; for (int i = 0; i < characters.Count; i++) { Character val2 = characters.Values[i]; if (!((Object)(object)val2 == (Object)null) && !((Object)(object)val2 == (Object)(object)val) && !(Vector3.Distance(((Component)val2).transform.position, ((Component)val).transform.position) > radius) && (string.IsNullOrEmpty(nameFilter) || (val2.Name != null && val2.Name.IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0))) { num++; if (!fix) { Dump(val2, "verb"); continue; } Dump(val2, "preFix"); GhostRig.Reinit(val2, "verb", force: true); Dump(val2, "postFix"); } } if (num == 0) { Plugin.Log.LogWarning((object)($"[GHOSTDIAG] no characters within {radius:0}m" + (string.IsNullOrEmpty(nameFilter) ? "" : (" matching '" + nameFilter + "'")) + ".")); } } internal static string Describe(Character ch, string tag) { //IL_0014: 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_0511: Unknown result type (might be due to invalid IL or missing references) //IL_0520: Unknown result type (might be due to invalid IL or missing references) //IL_052f: Unknown result type (might be due to invalid IL or missing references) //IL_0401: Unknown result type (might be due to invalid IL or missing references) //IL_0406: Unknown result type (might be due to invalid IL or missing references) //IL_0408: Unknown result type (might be due to invalid IL or missing references) //IL_040d: Unknown result type (might be due to invalid IL or missing references) //IL_0482: Unknown result type (might be due to invalid IL or missing references) //IL_0477: Unknown result type (might be due to invalid IL or missing references) //IL_0479: Unknown result type (might be due to invalid IL or missing references) //IL_047b: Unknown result type (might be due to invalid IL or missing references) //IL_0487: Unknown result type (might be due to invalid IL or missing references) //IL_0438: Unknown result type (might be due to invalid IL or missing references) //IL_043d: Unknown result type (might be due to invalid IL or missing references) //IL_044f: Unknown result type (might be due to invalid IL or missing references) //IL_0451: Unknown result type (might be due to invalid IL or missing references) //IL_0453: Unknown result type (might be due to invalid IL or missing references) //IL_0458: Unknown result type (might be due to invalid IL or missing references) //IL_045a: Unknown result type (might be due to invalid IL or missing references) //IL_045c: Unknown result type (might be due to invalid IL or missing references) //IL_045e: Unknown result type (might be due to invalid IL or missing references) //IL_0463: Unknown result type (might be due to invalid IL or missing references) //IL_0443: Unknown result type (might be due to invalid IL or missing references) //IL_0445: Unknown result type (might be due to invalid IL or missing references) //IL_0446: Unknown result type (might be due to invalid IL or missing references) //IL_0448: Unknown result type (might be due to invalid IL or missing references) //IL_074f: Unknown result type (might be due to invalid IL or missing references) //IL_0754: Unknown result type (might be due to invalid IL or missing references) //IL_0756: Unknown result type (might be due to invalid IL or missing references) //IL_075b: Unknown result type (might be due to invalid IL or missing references) //IL_0784: Unknown result type (might be due to invalid IL or missing references) //IL_0776: Unknown result type (might be due to invalid IL or missing references) //IL_0789: Unknown result type (might be due to invalid IL or missing references) //IL_07af: Unknown result type (might be due to invalid IL or missing references) //IL_07b4: Unknown result type (might be due to invalid IL or missing references) //IL_07b6: Unknown result type (might be due to invalid IL or missing references) //IL_07bb: Unknown result type (might be due to invalid IL or missing references) //IL_0823: Unknown result type (might be due to invalid IL or missing references) //IL_0818: Unknown result type (might be due to invalid IL or missing references) //IL_081a: Unknown result type (might be due to invalid IL or missing references) //IL_081c: Unknown result type (might be due to invalid IL or missing references) //IL_0828: Unknown result type (might be due to invalid IL or missing references) //IL_082b: Unknown result type (might be due to invalid IL or missing references) //IL_0830: Unknown result type (might be due to invalid IL or missing references) //IL_087a: Unknown result type (might be due to invalid IL or missing references) //IL_0886: Unknown result type (might be due to invalid IL or missing references) //IL_0892: Unknown result type (might be due to invalid IL or missing references) //IL_08ad: Unknown result type (might be due to invalid IL or missing references) //IL_08b9: Unknown result type (might be due to invalid IL or missing references) //IL_08c5: Unknown result type (might be due to invalid IL or missing references) //IL_08e2: Unknown result type (might be due to invalid IL or missing references) //IL_08e7: Unknown result type (might be due to invalid IL or missing references) //IL_08eb: Unknown result type (might be due to invalid IL or missing references) //IL_07e6: Unknown result type (might be due to invalid IL or missing references) //IL_07ea: Unknown result type (might be due to invalid IL or missing references) //IL_07ef: Unknown result type (might be due to invalid IL or missing references) //IL_07f4: Unknown result type (might be due to invalid IL or missing references) //IL_07f6: Unknown result type (might be due to invalid IL or missing references) //IL_07fa: Unknown result type (might be due to invalid IL or missing references) //IL_07ff: Unknown result type (might be due to invalid IL or missing references) //IL_0804: Unknown result type (might be due to invalid IL or missing references) //IL_07d7: Unknown result type (might be due to invalid IL or missing references) //IL_07dc: Unknown result type (might be due to invalid IL or missing references) //IL_07dd: Unknown result type (might be due to invalid IL or missing references) //IL_07df: Unknown result type (might be due to invalid IL or missing references) GameObject gameObject = ((Component)ch).gameObject; StringBuilder stringBuilder = new StringBuilder(); string text = "?"; try { text = ((object)ch.UID/*cast due to .constrained prefix*/).ToString(); } catch { } stringBuilder.Append("[GHOSTDIAG] " + tag + " '" + ch.Name + "' uid=" + text); CharacterVisuals visuals = ch.Visuals; GameObject val = (((Object)(object)visuals != (Object)null) ? ((Component)visuals).gameObject : null); stringBuilder.Append("\n init startInit=" + YN(ch.m_startInitDone) + " visualsHolder=" + (((Object)(object)visuals == (Object)null) ? "null" : ((Object)visuals).name)).Append(" holderActiveSelf=" + YN((Object)(object)val != (Object)null && val.activeSelf) + " holderActiveInHier=" + YN((Object)(object)val != (Object)null && val.activeInHierarchy)).Append(" defVisInit=" + YN((Object)(object)visuals != (Object)null && visuals.DefaultVisualsInitialized)) .Append(" visualDataNull=" + YN(ch.VisualData == null) + " prefabNull=" + YN((Object)(object)ch.CharacterVisualsPrefab == (Object)null)) .Append(" gameplayLoading=" + YN(IsGameplayLoading()) + " isAI=" + YN(ch.IsAI)); int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; if ((Object)(object)visuals != (Object)null) { Transform[] componentsInChildren = ((Component)visuals).GetComponentsInChildren<Transform>(true); foreach (Transform val2 in componentsInChildren) { num3++; if (((Component)val2).gameObject.activeSelf) { num4++; } if (((Object)val2).name.EndsWith("_v", StringComparison.Ordinal)) { num++; if (((Component)val2).gameObject.activeSelf) { num2++; } } } } stringBuilder.Append($"\n vtree _vNodes={num} _vActiveSelf={num2} visSubtreeActive={num4}/{num3}"); int num5 = LayerMask.NameToLayer("Hitbox"); Hitbox[] a = (((Object)(object)visuals != (Object)null) ? visuals.Hitboxes : null); Hitbox[] componentsInChildren2 = gameObject.GetComponentsInChildren<Hitbox>(true); stringBuilder.Append($"\n hits m_hitboxes={Len(ch.m_hitboxes)} visHitboxes={Len(a)} liveHitboxes={Len(componentsInChildren2)}").Append($" hitboxLayerIdx={num5} layers=[{LayerHistogram(componentsInChildren2)}]"); int num6 = 0; int num7 = 0; int num8 = 0; List<Collider> ragdollHitboxColliders = ch.m_ragdollHitboxColliders; if (ragdollHitboxColliders != null) { foreach (Collider item in ragdollHitboxColliders) { if (!((Object)(object)item == (Object)null)) { num8++; if (item.enabled) { num7++; } if (((Component)item).gameObject.layer == num5) { num6++; } } } } stringBuilder.Append("\n rag ragdollIsHitbox=" + YN(ch.RagdollIsHitbox) + " ragdollRoot=" + (((Object)(object)ch.RagdollRoot == (Object)null) ? "null" : ((Object)ch.RagdollRoot).name)).Append($" ragdollColliders={Count(ch.m_ragdollColliders)} ragdollHitboxColliders={num8}").Append($" onHitboxLayer={num6}/{num8} enabled={num7}/{num8} ragdollActive={YN(ch.RagdollActive)}"); Transform[] array = (((Object)(object)visuals != (Object)null) ? visuals.AttackTransforms : null); int num9 = 0; Vector3 val3 = Vector3.zero; Vector3 val4 = Vector3.zero; bool flag = true; if (array != null) { Transform[] array2 = array; foreach (Transform val5 in array2) { if (!((Object)(object)val5 == (Object)null)) { num9++; Vector3 position = val5.position; if (flag) { val3 = (val4 = position); flag = false; } else { val3 = Vector3.Min(val3, position); val4 = Vector3.Max(val4, position); } } } } Vector3 val6 = (flag ? Vector3.zero : (val4 - val3)); Weapon val7 = null; try { val7 = ch.CurrentWeapon; } catch { } Transform val8 = null; Transform val9 = null; Transform val10 = null; try { if ((Object)(object)val7 != (Object)null && (Object)(object)((Equipment)val7).EquippedVisuals != (Object)null) { val8 = ((Component)((Equipment)val7).EquippedVisuals).transform; val9 = val8.Find("_LinecastStart"); val10 = val8.Find("_LinecastEnd"); } } catch { } stringBuilder.Append($"\n atk attackTransforms={Len(array)} nonNull={num9} spread=({val6.x:0.0#}, {val6.y:0.0#}, {val6.z:0.0#})").Append(" weapon=" + (((Object)(object)val7 == (Object)null) ? "-" : ((Item)val7).Name) + " equippedVisuals=" + (((Object)(object)val8 == (Object)null) ? "null" : ((Object)val8).name)).Append(" linecastStart=" + (((Object)(object)val9 == (Object)null) ? "MISSING" : "found") + " linecastEnd=" + (((Object)(object)val10 == (Object)null) ? "MISSING" : "found")) .Append(" unarmedDetector=" + (((Object)(object)visuals != (Object)null && (Object)(object)visuals.UnarmedHitDetector != (Object)null) ? "set" : "null")); LockingPoint lockingPoint = ch.LockingPoint; GameObject val11 = (((Object)(object)lockingPoint != (Object)null) ? ((Component)lockingPoint).gameObject : null); Collider val12 = (((Object)(object)val11 != (Object)null) ? val11.GetComponent<Collider>() : null); stringBuilder.Append("\n lock lockingPoint=" + (((Object)(object)lockingPoint == (Object)null) ? "null" : ((Object)lockingPoint).name)).Append(" activeInHier=" + YN((Object)(object)val11 != (Object)null && val11.activeInHierarchy)).Append(" layer=" + (((Object)(object)val11 == (Object)null) ? "-" : LayerName(val11.layer))) .Append(" collider=" + (((Object)(object)val12 == (Object)null) ? "MISSING" : (val12.enabled ? "enabled" : "disabled"))) .Append(" inLockMask=" + (((Object)(object)val11 == (Object)null) ? "-" : YN((Global.LockingPointsMask & (1 << val11.layer)) != 0))); SkinnedMeshRenderer val13 = VisualPass.LargestSmr(gameObject); if ((Object)(object)val13 == (Object)null) { stringBuilder.Append("\n pose bodySmr=none"); } else { Transform[] bones = val13.bones; int num10 = 0; int num11 = 0; Vector3 val14 = Vector3.zero; Vector3 val15 = Vector3.zero; bool flag2 = true; Vector3 val16 = (((Object)(object)val13.rootBone != (Object)null) ? val13.rootBone.position : ((Component)val13).transform.position); Transform[] array3 = bones; foreach (Transform val17 in array3) { if ((Object)(object)val17 == (Object)null) { num11++; continue; } Vector3 val18 = val17.position - val16; if (((Vector3)(ref val18)).sqrMagnitude < 0.0001f) { num10++; } if (flag2) { val14 = (val15 = val17.position); flag2 = false; } else { val14 = Vector3.Min(val14, val17.position); val15 = Vector3.Max(val15, val17.position); } } Vector3 val19 = (flag2 ? Vector3.zero : (val15 - val14)); Census val20 = VisualPass.Measure(gameObject); StringBuilder stringBuilder2 = stringBuilder.Append($"\n pose bodySmr='{((Object)val13).name}' bones={Len(bones)} null={num11} stacked={num10}").Append($" boneSpread=({val19.x:0.0#}, {val19.y:0.0#}, {val19.z:0.0#})").Append($" baked=({val20.BakedX:0.0#}, {val20.BakedY:0.0#}, {val20.BakedZ:0.0#})"); Bounds localBounds = val13.localBounds; stringBuilder2.Append(string.Format(" localBounds={0} rootBone={1}", ((Bounds)(ref localBounds)).size, ((Object)(object)val13.rootBone == (Object)null) ? "null" : ((Object)val13.rootBone).name)).Append(" enabled=" + YN(((Renderer)val13).enabled) + " active=" + YN(((Component)val13).gameObject.activeInHierarchy)); } stringBuilder.Append("\n scale " + ScaleChain(val13, gameObject)); stringBuilder.Append(" | ghostly=" + YN(SafeGhostly(ch)) + " useLegacyVisual=" + YN(ch.UseLegacyVisual)); stringBuilder.Append("\n diag " + VisualPass.DiagStr(ch)); try { stringBuilder.Append('\n').Append(SkeletonRig.Census(gameObject)); } catch { } return stringBuilder.ToString(); } private static string YN(bool b) { if (!b) { return "F"; } return "T"; } private static string ScaleChain(SkinnedMeshRenderer body, GameObject root) { //IL_0155: 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_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0062: 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_0167: Unknown result type (might be due to invalid IL or missing references) //IL_016e: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Unknown result type (might be due to invalid IL or missing references) //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_0162: 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_008a: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: 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_00ec: Unknown result type (might be due to invalid IL or missing references) try { Transform val = (((Object)(object)body != (Object)null && (Object)(object)body.rootBone != (Object)null) ? body.rootBone : (((Object)(object)body != (Object)null) ? ((Component)body).transform : null)); if ((Object)(object)val == (Object)null) { return "no rootBone — nothing to walk"; } StringBuilder stringBuilder = new StringBuilder(); int num = 0; while ((Object)(object)val != (Object)null && num++ < 24) { Vector3 localScale = val.localScale; Vector3 lossyScale = val.lossyScale; bool flag = Mathf.Abs(localScale.x) < 0.1f || Mathf.Abs(localScale.y) < 0.1f || Mathf.Abs(localScale.z) < 0.1f; if (stringBuilder.Length > 0) { stringBuilder.Append(" < "); } stringBuilder.Append($"{((Object)val).name}[{localScale.x:0.0##},{localScale.y:0.0##},{localScale.z:0.0##}]"); if (flag) { stringBuilder.Append("<<DEGENERATE"); } if ((Object)(object)((Component)val).gameObject == (Object)(object)root) { break; } val = val.parent; } Vector3 val2 = (((Object)(object)body != (Object)null && (Object)(object)((Component)body).transform != (Object)null) ? ((Component)body).transform.lossyScale : Vector3.zero); return stringBuilder.Append($" | smrLossy=({val2.x:0.0##}, {val2.y:0.0##}, {val2.z:0.0##})").ToString(); } catch (Exception ex) { return "threw (" + ex.GetType().Name + ")"; } } private static bool SafeGhostly(Character ch) { try { return ch.Ghostly; } catch { return false; } } private static string LayerName(int layer) { string text = LayerMask.LayerToName(layer); if (!string.IsNullOrEmpty(text)) { return text; } return layer.ToString(); } private static int Len(Array a) { return a?.Length ?? (-1); } private static int Count(ICollection c) { return c?.Count ?? (-1); } private static string LayerHistogram(Hitbox[] hits) { if (hits == null || hits.Length == 0) { return ""; } Dictionary<string, int> dictionary = new Dictionary<string, int>(); foreach (Hitbox val in hits) { if (!((Object)(object)val == (Object)null)) { string text = LayerMask.LayerToName(((Component)val).gameObject.layer); if (string.IsNullOrEmpty(text)) { text = ((Component)val).gameObject.layer.ToString(); } dictionary[text] = ((!dictionary.TryGetValue(text, out var value)) ? 1 : (value + 1)); } } StringBuilder stringBuilder = new StringBuilder(); foreach (KeyValuePair<string, int> item in dictionary) { if (stringBuilder.Length > 0) { stringBuilder.Append(", "); } stringBuilder.Append(item.Key).Append(" x").Append(item.Value); } return stringBuilder.ToString(); } } internal static class GhostRig { internal static bool Enabled { get { if (Plugin.RigReinitPass != null) { return Plugin.RigReinitPass.Value; } return false; } } internal static bool Reinit(Character ch, string why, bool force = false) { //IL_066f: Unknown result type (might be due to invalid IL or missing references) //IL_0671: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_011e: 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) //IL_009d: 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_00c0: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ch == (Object)null || (Object)(object)((Component)ch).gameObject == (Object)null) { return false; } GameObject gameObject = ((Component)ch).gameObject; List<string> list = new List<string>(); RigCensus val = Census(ch); string text = default(string); bool flag = RigGate.NeedsReinit(ref val, ref text); if (!flag && !force) { if (GhostDiag.Enabled) { Plugin.Log.LogMessage((object)("[GHOSTFIX] '" + ch.Name + "' (" + why + ") steps=none — gate=" + text + " " + $"(hitboxes {val.CapturedHitboxes}/{val.LiveHitboxes}, ragdollHitbox {val.RagdollHitboxColliders}, " + $"attackXforms {val.AttackTransforms}).")); } return false; } string text2 = ((force && !flag) ? "forced (gate=healthy)" : text); int hitsBefore = Len(ch.m_hitboxes); int rhbBefore = Count(ch.m_ragdollHitboxColliders); int atkBefore = AtkCount(ch); Vector3 spreadBefore = BoneSpread(gameObject); Census censusBefore = VisualPass.Measure(gameObject); try { int num = ActivateRigSubtree(gameObject); if (num > 0) { list.Add($"activate({num})"); } CharacterVisuals visuals = ch.Visuals; if ((Object)(object)visuals == (Object)null && !ch.m_startInitDone) { if ((Object)(object)ch.CharacterVisualsPrefab == (Object)null && ch.VisualData == null && (Object)(object)((Component)ch).GetComponentInChildren<CharacterVisuals>(true) == (Object)null) { list.Add("processInit(SKIPPED:no visuals source)"); Plugin.Log.LogWarning((object)("[GHOSTFIX] '" + ch.Name + "' (" + why + ") has no visuals prefab, no VisualData and no CharacterVisuals in its hierarchy — ProcessInit would NRE, skipping (this body should have been refused as a spawn template upstream).")); Log(ch, why, "no visuals holder — nothing to re-init", list); return false; } try { ch.ProcessInit(); visuals = ch.Visuals; list.Add("processInit(startInit=" + (ch.m_startInitDone ? "T" : "F") + " holder=" + (((Object)(object)visuals == (Object)null) ? "null" : ((Object)visuals).name) + ")"); } catch (Exception ex) { list.Add("processInit(threw:" + ex.GetType().Name + ")"); Plugin.Log.LogWarning((object)("[GHOSTFIX] '" + ch.Name + "' (" + why + ") ProcessInit threw (" + ex.GetType().Name + ": " + ex.Message + ") — the body keeps its un-initialized vanilla state.")); } } if ((Object)(object)visuals == (Object)null) { Log(ch, why, "no visuals holder — nothing to re-init", list); return false; } visuals.m_character = ch; if (!visuals.DefaultVisualsInitialized) { if (ch.VisualData == null) { list.Add("defVis(SKIPPED:VisualData null)"); } else { visuals.InitDefaultVisuals(); list.Add("defVis"); } } visuals.InitHitboxes(); list.Add("initHitboxes"); ch.m_pelvis = visuals.RagdollRoot; ch.RagdollRoot = visuals.RagdollRoot; ch.m_hitboxes = visuals.Hitboxes; ch.m_dodgeHitboxes = visuals.DodgeHitboxes; if (ch.m_hitboxes != null) { Hitbox[] hitboxes = ch.m_hitboxes; foreach (Hitbox val2 in hitboxes) { if ((Object)(object)val2 != (Object)null) { val2.OwnerChar = ch; } } } list.Add($"relatch(hitboxes={Len(ch.m_hitboxes)})"); string text3 = default(string); if (RigGate.NeedsRagdollInit(ref val, ref text3)) { int num2 = ClearRagdollCaches(ch); if (num2 < 0) { list.Add("ragdollSkip(clear refused)"); } else { ch.InitRagdoll(); list.Add((num2 > 0) ? $"initRagdoll(recleared {num2})" : "initRagdoll"); ch.SetRagdollActive(false); list.Add("setRagdollActive(false)"); } } else { list.Add("ragdollSkip(" + text3 + ")"); } Animator componentInChildren = gameObject.GetComponentInChildren<Animator>(true); if ((Object)(object)componentInChildren != (Object)null && (Object)(object)componentInChildren.avatar != (Object)null && componentInChildren.avatar.isValid) { if (!((Behaviour)componentInChildren).enabled) { ((Behaviour)componentInChildren).enabled = true; list.Add("animEnable"); } componentInChildren.cullingMode = (AnimatorCullingMode)0; componentInChildren.Rebind(); componentInChildren.Update(0f); componentInChildren.Update((Time.fixedDeltaTime > 0f) ? Time.fixedDeltaTime : 0.02f); list.Add("rebind+tick"); } int num3 = 0; SkinnedMeshRenderer[] componentsInChildren = gameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true); foreach (SkinnedMeshRenderer val3 in componentsInChildren) { if ((Object)(object)val3 != (Object)null && !val3.updateWhenOffscreen) { val3.updateWhenOffscreen = true; num3++; } } if (num3 > 0) { list.Add($"updOff({num3})"); } if ((Object)(object)ch.LockingPoint != (Object)null && !((Component)ch.LockingPoint).gameObject.activeSelf) { ((Component)ch.LockingPoint).gameObject.SetActive(true); list.Add("lockingPoint"); } try { int num4 = HumanoidWeapon.EnsureEquipped(ch); if (num4 > 0) { list.Add($"reEquip({num4})"); } } catch (Exception ex2) { list.Add("reEquip(threw:" + ex2.GetType().Name + ")"); } try { int num5 = CasterSkills.EnsureLearned(ch); if (num5 > 0) { list.Add($"reLearn({num5})"); } } catch (Exception ex3) { list.Add("reLearn(threw:" + ex3.GetType().Name + ")"); } } catch (Exception ex4) { Plugin.Log.LogWarning((object)string.Format("[GHOSTFIX] '{0}' ({1}) threw after [{2}]: {3}", ch.Name, why, string.Join(", ", list.ToArray()), ex4)); } LogResult(ch, why + " gate=" + text2, list, hitsBefore, rhbBefore, atkBefore, spreadBefore, censusBefore); return list.Count > 0; } private static RigCensus Census(Character ch) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) RigCensus val = default(RigCensus); try { GameObject gameObject = ((Component)ch).gameObject; CharacterVisuals visuals = ch.Visuals; val.StartInitMissing = !ch.m_startInitDone; val.LiveHitboxes = gameObject.GetComponentsInChildren<Hitbox>(true).Length; val.CapturedHitboxes = (((Object)(object)visuals != (Object)null && visuals.Hitboxes != null) ? visuals.Hitboxes.Length : 0); val.HasRagdollRoot = (Object)(object)ch.RagdollRoot != (Object)null; val.RagdollIsHitbox = ch.RagdollIsHitbox; val.RagdollHitboxColliders = ((ch.m_ragdollHitboxColliders != null) ? ch.m_ragdollHitboxColliders.Count : 0); val.AttackTransforms = AtkCount(ch); if (val.AttackTransforms < 0) { val.AttackTransforms = 0; } val.InactiveRigObjects = CountInactiveRigObjects(gameObject); val.LockingPointAsleep = (Object)(object)ch.LockingPoint != (Object)null && !((Component)ch.LockingPoint).gameObject.activeSelf; val.RagdollRigidbodyCache = ((ch.m_ragdollRigidbodies != null) ? ch.m_ragdollRigidbodies.Count : 0); CharacterJointManager[] componentsInChildren = gameObject.GetComponentsInChildren<CharacterJointManager>(true); foreach (CharacterJointManager val2 in componentsInChildren) { if (!((Object)(object)val2 == (Object)null)) { val.RagdollJointManagers++; if (val2.m_hasJoint) { val.RagdollManagersWithJoint++; } } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[GHOSTFIX] census threw: " + ex.Message)); } return val; } private static int CountInactiveRigObjects(GameObject go) { int num = 0; Transform[] componentsInChildren = go.GetComponentsInChildren<Transform>(true); foreach (Transform val in componentsInChildren) { if (!((Object)(object)val == (Object)null) && !((Component)val).gameObject.activeSelf) { GameObject gameObject = ((Component)val).gameObject; if ((Object)(object)gameObject.GetComponent<Hitbox>() != (Object)null || (Object)(object)gameObject.GetComponent<Collider>() != (Object)null || (Object)(object)gameObject.GetComponent<Renderer>() != (Object)null || (Object)(object)gameObject.GetComponent<CharacterVisuals>() != (Object)null) { num++; } } } return num; } private static int ActivateRigSubtree(GameObject go) { int num = 0; Transform[] componentsInChildren = go.GetComponentsInChildren<Transform>(true); foreach (Transform val in componentsInChildren) { if (!((Object)(object)val == (Object)null) && !((Component)val).gameObject.activeSelf) { GameObject gameObject = ((Component)val).gameObject; if (!((Object)(object)gameObject.GetComponent<Hitbox>() == (Object)null) || !((Object)(object)gameObject.GetComponent<Collider>() == (Object)null) || !((Object)(object)gameObject.GetComponent<Renderer>() == (Object)null) || !((Object)(object)gameObject.GetComponent<CharacterVisuals>() == (Object)null)) { gameObject.SetActive(true); num++; } } } return num; } private static int ClearRagdollCaches(Character ch) { int num = 0; if (ch.m_ragdollRigidbodies != null && ch.m_ragdollRigidbodies.Count > 0) { foreach (Rigidbody ragdollRigidbody in ch.m_ragdollRigidbodies) { if ((Object)(object)ragdollRigidbody == (Object)null) { continue; } CharacterJointManager[] components = ((Component)ragdollRigidbody).GetComponents<CharacterJointManager>(); foreach (CharacterJointManager val in components) { if ((Object)(object)val != (Object)null && val.m_hasJoint) { Plugin.Log.LogError((object)("[GHOSTFIX] '" + ch.Name + "': REFUSED to clear ragdoll caches — a CharacterJointManager still holds a joint config (BUG-RAGDOLLJOINTLOSS). Leaving the ragdoll as-is.")); return -1; } } } num += ch.m_ragdollRigidbodies.Count; foreach (Rigidbody ragdollRigidbody2 in ch.m_ragdollRigidbodies) { if (!((Object)(object)ragdollRigidbody2 == (Object)null)) { CharacterJointManager[] components2 = ((Component)ragdollRigidbody2).GetComponents<CharacterJointManager>(); foreach (CharacterJointManager val2 in components2) { Object.Destroy((Object)(object)val2); } } } ch.m_ragdollRigidbodies.Clear(); } if (ch.m_ragdollCharacterJointManagers != null) { ch.m_ragdollCharacterJointManagers.Clear(); } if (ch.m_ragdollColliders != null) { num += ch.m_ragdollColliders.Count; ch.m_ragdollColliders.Clear(); } if (ch.m_ragdollHitboxColliders != null) { num += ch.m_ragdollHitboxColliders.Count; ch.m_ragdollHitboxColliders.Clear(); } return num; } private static void LogResult(Character ch, string why, List<string> steps, int hitsBefore, int rhbBefore, int atkBefore, Vector3 spreadBefore, Census censusBefore) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: 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_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0174: 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_0192: Unknown result type (might be due to invalid IL or missing references) //IL_01a1: Unknown result type (might be due to invalid IL or missing references) //IL_01af: Unknown result type (might be due to invalid IL or missing references) //IL_01bd: Unknown result type (might be due to invalid IL or missing references) //IL_01e0: Unknown result type (might be due to invalid IL or missing references) //IL_01ef: Unknown result type (might be due to invalid IL or missing references) //IL_01fe: Unknown result type (might be due to invalid IL or missing references) //IL_020d: Unknown result type (might be due to invalid IL or missing references) //IL_021b: Unknown result type (might be due to invalid IL or missing references) //IL_0229: Unknown result type (might be due to invalid IL or missing references) try { GameObject gameObject = ((Component)ch).gameObject; Census val = VisualPass.Measure(gameObject); Vector3 val2 = BoneSpread(gameObject); int num = LayerMask.NameToLayer("Hitbox"); int num2 = 0; int num3 = 0; if (ch.m_ragdollHitboxColliders != null) { foreach (Collider ragdollHitboxCollider in ch.m_ragdollHitboxColliders) { if (!((Object)(object)ragdollHitboxCollider == (Object)null)) { num3++; if (((Component)ragdollHitboxCollider).gameObject.layer == num) { num2++; } } } } StringBuil