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 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(true); List list = new List(); List list2 = new List(); Skill[] array = componentsInChildren; foreach (Skill val in array) { if ((Object)(object)val != (Object)null) { list.Add(((Item)val).ItemID); } } IList learnedItems = ((CharacterKnowledge)knowledge).GetLearnedItems(); if (learnedItems != null) { foreach (Item item in learnedItems) { if ((Object)(object)item != (Object)null) { list2.Add(item.ItemID); } } } List list3 = CasterSkillGate.MissingSkillIds((IEnumerable)list2, (IEnumerable)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(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() : 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 hashSet = new HashSet(); Skill[] componentsInChildren = ((Component)knowledge).GetComponentsInChildren(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(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 _spawns = new List(); 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 _coldDegradedLoggedAt = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary _gateNotEvaluatedLoggedAt = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly List _watchScratch = new List(); 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, LeaseVerdict, float>)AgedOutNotice, (Action)CorpseMuteReassertFailed); } private static void AgedOutNotice(LeaseEntry 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 list = default(List); return SpeciesTable.TryResolveKey>(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(template, MintHolder().transform); int viewId = -1; try { ((Object)val).name = ((Object)template).name.Replace("SK_Template_", "SK_Spawn_"); Character component = val.GetComponent(); 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(); 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(); 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(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() != (Object)null} " + $"charAIDisable={(Object)(object)val.GetComponent() != (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 Snapshot(string ownerTag) { List list = new List(); Snapshot(ownerTag, list); return list; } internal static void Snapshot(string ownerTag, List 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(); 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 list; if (flag) { _watchScratchBusy = true; _watchScratch.Clear(); list = _watchScratch; } else { list = new List(_spawns.Count); } list.AddRange(_spawns); try { WatchWalk(list); } finally { if (flag) { _watchScratch.Clear(); _watchScratchBusy = false; } } } private static void WatchWalk(List 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 onDone, bool force = false) { string key = (speciesKey ?? "").Trim(); bool fired = false; Action 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 list = default(List); if (!SpeciesTable.TryResolveKey>(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 list2 = default(List); 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)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 ' 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 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(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(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 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() : 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("< dictionary = new Dictionary(); 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 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 list = new List(); 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(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(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(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(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(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(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() != (Object)null || (Object)(object)gameObject.GetComponent() != (Object)null || (Object)(object)gameObject.GetComponent() != (Object)null || (Object)(object)gameObject.GetComponent() != (Object)null) { num++; } } } return num; } private static int ActivateRigSubtree(GameObject go) { int num = 0; Transform[] componentsInChildren = go.GetComponentsInChildren(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() == (Object)null) || !((Object)(object)gameObject.GetComponent() == (Object)null) || !((Object)(object)gameObject.GetComponent() == (Object)null) || !((Object)(object)gameObject.GetComponent() == (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(); 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(); 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 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++; } } } } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("[GHOSTFIX] '" + ch.Name + "' (" + why + ") steps=" + ((steps.Count == 0) ? "none" : string.Join("+", steps.ToArray()))).Append($" | hitboxes {hitsBefore}->{Len(ch.m_hitboxes)}").Append($" ragdollHitbox {rhbBefore}->{num3} (onHitboxLayer={num2}/{num3})") .Append($" attackXforms {atkBefore}->{AtkCount(ch)}") .Append($" boneSpread ({spreadBefore.x:0.0#}, {spreadBefore.y:0.0#}, {spreadBefore.z:0.0#})->({val2.x:0.0#}, {val2.y:0.0#}, {val2.z:0.0#})") .Append($" baked ({censusBefore.BakedX:0.0#}, {censusBefore.BakedY:0.0#}, {censusBefore.BakedZ:0.0#})->({val.BakedX:0.0#}, {val.BakedY:0.0#}, {val.BakedZ:0.0#})"); Plugin.Log.LogMessage((object)stringBuilder.ToString()); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[GHOSTFIX] result line threw: " + ex.Message)); } } private static void Log(Character ch, string why, string msg, List steps) { Plugin.Log.LogWarning((object)("[GHOSTFIX] '" + ch.Name + "' (" + why + ") " + msg + " — steps=" + ((steps.Count == 0) ? "none" : string.Join("+", steps.ToArray())))); } private static Vector3 BoneSpread(GameObject go) { //IL_0016: 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_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) SkinnedMeshRenderer val = VisualPass.LargestSmr(go); if ((Object)(object)val == (Object)null) { return Vector3.zero; } Vector3 val2 = Vector3.zero; Vector3 val3 = Vector3.zero; bool flag = true; Transform[] bones = val.bones; foreach (Transform val4 in bones) { if (!((Object)(object)val4 == (Object)null)) { if (flag) { val2 = (val3 = val4.position); flag = false; } else { val2 = Vector3.Min(val2, val4.position); val3 = Vector3.Max(val3, val4.position); } } } if (!flag) { return val3 - val2; } return Vector3.zero; } private static int AtkCount(Character ch) { Transform[] array = (((Object)(object)ch != (Object)null && (Object)(object)ch.Visuals != (Object)null) ? ch.Visuals.AttackTransforms : null); if (array == null) { return -1; } int num = 0; Transform[] array2 = array; foreach (Transform val in array2) { if ((Object)(object)val != (Object)null) { num++; } } return num; } private static int Len(Array a) { return a?.Length ?? (-1); } private static int Count(ICollection c) { return c?.Count ?? (-1); } } internal static class GhostVisuals { [HarmonyPatch(typeof(Character), "InitVisuals")] internal static class InitVisualsPatch { private static void Postfix(Character __instance) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) if (!On) { return; } try { if ((Object)(object)__instance == (Object)null || __instance.UseLegacyVisual) { return; } string text = null; try { text = ((object)__instance.UID/*cast due to .constrained prefix*/).ToString(); } catch { } if (!SpawnUid.IsSpawnUid(text)) { return; } CharacterVisuals visualsHolder = __instance.m_visualsHolder; if (!((Object)(object)visualsHolder == (Object)null) && !visualsHolder.DefaultVisualsInitialized) { visualsHolder.InitDefaultVisuals(); if ((Object)(object)__instance.m_animator != (Object)null) { __instance.m_animator.Rebind(); } Plugin.Log.LogMessage((object)("[SPAWN] ghost-visuals F2: ran InitDefaultVisuals for spawn uid=" + text + " ('holder already exists' / IsGameplayLoading&&IsAI — both vanilla routes skip it for a cloned dynamic-visuals body) + animator Rebind.")); } } catch (Exception ex) { ModLog log = Plugin.Log; if (log != null) { log.LogWarning((object)("[SPAWN] ghost-visuals F2 threw (" + ex.GetType().Name + ": " + ex.Message + ") — vanilla result kept.")); } } } } [HarmonyPatch(typeof(Character), "Visible", new Type[] { typeof(bool) })] internal static class VisiblePatch { private static void Postfix(Character __instance, bool _visible) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) if (!On) { return; } try { if ((Object)(object)__instance == (Object)null || __instance.UseLegacyVisual) { return; } string text = null; try { text = ((object)__instance.UID/*cast due to .constrained prefix*/).ToString(); } catch { } if (!SpawnUid.IsSpawnUid(text)) { return; } string text2 = "-"; try { Animator animator = __instance.m_animator; if ((Object)(object)animator != (Object)null && ((Behaviour)animator).isActiveAndEnabled && (Object)(object)animator.runtimeAnimatorController != (Object)null) { AnimatorClipInfo[] currentAnimatorClipInfo = animator.GetCurrentAnimatorClipInfo(0); if (currentAnimatorClipInfo != null && currentAnimatorClipInfo.Length != 0 && (Object)(object)((AnimatorClipInfo)(ref currentAnimatorClipInfo[0])).clip != (Object)null) { text2 = ((Object)((AnimatorClipInfo)(ref currentAnimatorClipInfo[0])).clip).name; } } } catch { } Plugin.Log.LogMessage((object)("[VISIBLE] '" + __instance.Name + "' uid=" + text + " -> " + (_visible ? "SHOWN" : "HIDDEN") + " (holder=" + (((Object)(object)__instance.m_visualsHolder == (Object)null) ? "null" : ((Object)__instance.m_visualsHolder).name) + ", clip='" + text2 + "') — vanilla animation-event visibility, not our code.")); } catch (Exception ex) { ModLog log = Plugin.Log; if (log != null) { log.LogWarning((object)("[VISIBLE] probe threw (" + ex.GetType().Name + ": " + ex.Message + ").")); } } } } private static bool On { get { if (Plugin.GhostVisualsFix != null) { return Plugin.GhostVisualsFix.Value; } return false; } } internal static void PreActivationFix(GameObject go, Character ch) { if (!On) { return; } try { if ((Object)(object)ch == (Object)null || ch.UseLegacyVisual) { return; } CharacterVisuals componentInChildren = go.GetComponentInChildren(true); if ((Object)(object)componentInChildren == (Object)null) { return; } int num = 0; Transform[] componentsInChildren = ((Component)componentInChildren).GetComponentsInChildren(true); foreach (Transform val in componentsInChildren) { if (!((Component)val).gameObject.activeSelf) { ((Component)val).gameObject.SetActive(true); num++; } } Transform ragdollRoot = componentInChildren.RagdollRoot; if ((Object)(object)ragdollRoot != (Object)null) { Transform[] componentsInChildren2 = ((Component)ragdollRoot).GetComponentsInChildren(true); foreach (Transform val2 in componentsInChildren2) { if (!((Component)val2).gameObject.activeSelf) { ((Component)val2).gameObject.SetActive(true); num++; } } } if (num > 0) { Plugin.Log.LogMessage((object)($"[SPAWN] ghost-visuals F1: force-activated {num} inactive node(s) under " + "'" + ((Object)componentInChildren).name + "' on '" + ((Object)go).name + "' pre-activation (UseLegacyVisual=false — Character.Start's active-only init chain can now see the whole subtree).")); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SPAWN] ghost-visuals F1 threw (" + ex.GetType().Name + ": " + ex.Message + ") — clone continues un-fixed.")); } } internal static void RootScaleFix(GameObject go, Character ch) { //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_009a: 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_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) if (!On) { return; } try { if ((Object)(object)ch == (Object)null || ch.UseLegacyVisual) { return; } string text = null; try { text = ((object)ch.UID/*cast due to .constrained prefix*/).ToString(); } catch { } if (SpawnUid.IsSpawnUid(text)) { Vector3 localScale = go.transform.localScale; if (RootScale.NeedsFix(localScale.x, localScale.y, localScale.z)) { float num = default(float); float num2 = default(float); float num3 = default(float); RootScale.Fix(localScale.x, localScale.y, localScale.z, ref num, ref num2, ref num3); go.transform.localScale = new Vector3(num, num2, num3); Plugin.Log.LogMessage((object)($"[GHOSTFIX] root scale ({localScale.x:F2},{localScale.y:F2},{localScale.z:F2}) -> ({num:F2},{num2:F2},{num3:F2}) " + "on '" + ((Object)go).name + "' uid=" + text + " — the donor's own authored root localScale rode the clone in (Instantiate copies local transform values verbatim) and crushed the whole skinned body to a sliver. Degenerate axes only (<" + 0.1f.ToString("F2") + "); UseLegacyVisual=false + SK_ uid scoped, so baked beasts are untouched.")); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[GHOSTFIX] root-scale fix threw (" + ex.GetType().Name + ": " + ex.Message + ") — clone continues un-fixed.")); } } } internal static class GuestReplicas { internal enum ReplicaState { Mirroring, Active, Dead } internal sealed class Replica { public string Uid; public int ViewId; public string SpeciesKey; public Character Character; public ReplicaState State; public string Source; public string ConsumerData = ""; public float ActiveAt; public int EnforceRepeats; public int InitNudgeAttempts; public bool InitConvergedLogged; public bool InitBlockedLogged; public string ShippedRhUid = ""; public string ShippedLhUid = ""; public int WeaponForced; public int VisualPassAttempts; public bool VisualConvergedLogged; public float NextEnforceAt; public float EnforceInterval; } private sealed class PendingMirror { public SpawnMsg Msg; public float QueuedAt; public string LastWait = ""; } private static readonly Dictionary _replicas = new Dictionary(StringComparer.Ordinal); private static readonly List _queue = new List(); private static readonly MirrorInFlight _inFlight = new MirrorInFlight(); private static int _harvestsThisSession; private static readonly GuestHarvestStanding _standing = new GuestHarvestStanding(); private static float _nextWarmAttempt; private const float WarmRetrySeconds = 5f; private static float _nextWatch; private const float EnforceBackoffMaxSeconds = 4f; private static GameObject _holder; private static readonly Dictionary>> _voluntaryDone = new Dictionary>>(StringComparer.OrdinalIgnoreCase); private static string _warmingKey; internal const int UnlimitedBudgetSentinel = 99; private static string _followUid; private static readonly List _watchSnapshot = new List(); internal static int Count => _replicas.Count; internal static int QueueCount => _queue.Count; private static GameObject Holder() { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown if ((Object)(object)_holder == (Object)null) { _holder = new GameObject("SK_GuestMintHolder"); _holder.SetActive(false); } return _holder; } internal static void OnSpawnMessage(string payload, int actor, string expectedUid = null) { //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: 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_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0243: Unknown result type (might be due to invalid IL or missing references) //IL_0244: Unknown result type (might be due to invalid IL or missing references) //IL_01af: Unknown result type (might be due to invalid IL or missing references) //IL_01c0: Unknown result type (might be due to invalid IL or missing references) //IL_0227: Unknown result type (might be due to invalid IL or missing references) SpawnMsg val = default(SpawnMsg); if (!SpawnNetProtocol.TryDecodeSpawn(payload, ref val)) { SpawnNet.CountDrop("sk.spawn", "unparseable"); Plugin.Log.LogWarning((object)$"[MIRROR] malformed sk.spawn from actor {actor}: '{payload}'."); return; } if (expectedUid != null && !string.Equals(val.Uid, expectedUid, StringComparison.Ordinal)) { SpawnNet.CountDrop("sk.spawn", "key-mismatch"); Plugin.Log.LogWarning((object)("[MIRROR] sk.spawn record key '" + expectedUid + "' disagrees with the payload's embedded uid '" + val.Uid + "' — dropped (inconsistent message; minting under either identity would be a guess).")); return; } if (val.Proto != 1) { SpawnNet.CountDrop("sk.spawn", "proto-skew"); Plugin.Log.LogWarning((object)$"[MIRROR] sk.spawn uid={val.Uid} carries protocol v{val.Proto} (ours v{1}) — dropped."); return; } if (IsKnown(val.Uid)) { SpawnNet.CountDrop("sk.spawn", "dup"); if (Plugin.VerboseNet.Value) { Plugin.Log.LogInfo((object)("[MIRROR] duplicate sk.spawn uid=" + val.Uid + " — dropped (already queued/minted).")); } return; } int num = ((Plugin.MaxActiveSpawns != null) ? Plugin.MaxActiveSpawns.Value : 12); int num2 = 0; foreach (Replica value in _replicas.Values) { if (value.State != ReplicaState.Dead) { num2++; } } if (num2 + _queue.Count + 1 > num) { SpawnNet.CountDrop("sk.spawn", "cap"); Plugin.Log.LogWarning((object)("[MIRROR] refuse sk.spawn uid=" + val.Uid + " species='" + val.SpeciesKey + "' — replica cap " + $"({num2} live + {_queue.Count} queued, cap {num} = [Spawner] MaxActiveSpawns; " + $"{_replicas.Count - num2} dead corpse(s) excluded). " + "The master's own cap should have bounded this — version skew? sk.fail(cap) sent.")); SendFail(val.Uid, "cap"); } else { _queue.Add(new PendingMirror { Msg = val, QueuedAt = Time.unscaledTime }); TryProcessQueue(); } } internal static void OnGoneMessage(string payload, int actor, string verb) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) GoneMsg val = default(GoneMsg); if (!SpawnNetProtocol.TryDecodeGone(payload, ref val)) { SpawnNet.CountDrop(verb, "unparseable-or-skew"); Plugin.Log.LogWarning((object)$"[MIRROR] malformed/unknown-kind gone from actor {actor}: '{payload}' (newer sender?)."); } else { ApplyGone(val.Uid, val.Kind, $"gone({val.Kind})", countUnknown: true, verb); } } internal static void OnRecordCleared(string uid, string reason, int actor) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005f: 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_003a: Unknown result type (might be due to invalid IL or missing references) GoneMsg val = default(GoneMsg); if (SpawnNetProtocol.TryParseGoneReason(reason, ref val)) { if (!string.Equals(val.Uid, uid, StringComparison.Ordinal)) { Plugin.Log.LogWarning((object)("[MIRROR] gone record key '" + uid + "' disagrees with the release payload's embedded uid '" + val.Uid + "' — acting on the record key (it is what the row was stored under).")); } ApplyGone(uid, val.Kind, $"gone({val.Kind})", countUnknown: true, "sk.gone"); } else { ApplyGone(uid, (GoneKind)1, reason, countUnknown: false, "sk.gone"); } } private static void ApplyGone(string uid, GoneKind kind, string why, bool countUnknown, string verb) { //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Invalid comparison between Unknown and I4 //IL_015d: Unknown result type (might be due to invalid IL or missing references) for (int num = _queue.Count - 1; num >= 0; num--) { if (string.Equals(_queue[num].Msg.Uid, uid, StringComparison.Ordinal)) { _queue.RemoveAt(num); Plugin.Log.LogMessage((object)("[MIRROR] gone uid=" + uid + " (" + why + ") — was still queued, dequeued (never minted here).")); return; } } if (!_replicas.TryGetValue(uid, out var value)) { if (_inFlight.RecordGone(uid, kind)) { Plugin.Log.LogMessage((object)("[MIRROR] gone uid=" + uid + " (" + why + ") — arrived mid-mint; tombstoned, the mirror routine will apply it when the body exists (no orphan replica).")); } else if (countUnknown) { SpawnNet.CountDrop("sk.gone", "unknown-uid"); if (Plugin.VerboseNet.Value) { Plugin.Log.LogInfo((object)("[MIRROR] gone uid=" + uid + " (" + why + ") — unknown here (already pruned?).")); } } return; } Character character = value.Character; if ((int)kind != 0) { if (kind - 1 <= 1) { RemoveReplica(value, why); } return; } if ((Object)(object)character != (Object)null && !character.IsDead) { Plugin.Log.LogMessage((object)("[MIRROR] self-heal uid=" + uid + ": master says died but replica alive — local Die().")); try { character.Die(Vector3.up, false); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[MIRROR] self-heal Die() threw: " + ex.Message)); } } value.State = ReplicaState.Dead; } private static bool IsKnown(string uid) { if (_replicas.ContainsKey(uid)) { return true; } if (_inFlight.IsInFlight(uid)) { return true; } foreach (PendingMirror item in _queue) { if (string.Equals(item.Msg.Uid, uid, StringComparison.Ordinal)) { return true; } } return false; } internal static void Tick() { if (_queue.Count > 0) { TryProcessQueue(); } if (_standing.WantedCount > 0) { TryWarmWanted(); } FollowTick(); if (!(Time.unscaledTime < _nextWatch)) { _nextWatch = Time.unscaledTime + 0.5f; WatchTick(); } } internal static bool HarvestSafeNow(out string why) { return HarvestSafeNow((WantClass)0, out why); } internal static bool HarvestSafeNow(WantClass cls, out string why) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) string detail; GuestWarmVerdict val = Decide(cls, out detail); if ((int)val == 0) { why = ""; return true; } why = detail; return false; } private static int MirrorBudget() { if (Plugin.MirrorHarvestBudget == null) { return 4; } return Plugin.MirrorHarvestBudget.Value; } private static int VoluntaryBudget() { if (Plugin.GuestPrewarmBudget == null) { return 4; } return Plugin.GuestPrewarmBudget.Value; } private static int SessionCeiling() { if (Plugin.GuestDonorCycleCeiling == null) { return 10; } return Plugin.GuestDonorCycleCeiling.Value; } private static bool GuestPrewarmEnabled() { if (Plugin.GuestPrewarm != null) { return Plugin.GuestPrewarm.Value; } return true; } private static GuestWarmVerdict Decide(WantClass cls, out string detail) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Expected I4, but got Unknown //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Invalid comparison between Unknown and I4 //IL_014e: Unknown result type (might be due to invalid IL or missing references) string text = default(string); bool flag = HarvestAdmission.SafeToStart(ref text); bool flag2 = false; try { Character val = default(Character); flag2 = Lifecycle.TryGetFirstLocalCharacter(ref val) && (Object)(object)val != (Object)null && val.InCombat; } catch { } int cyclesThisSession = DonorHarvest.CyclesThisSession; GuestWarmVerdict val2 = GuestWarmPolicy.Decide(cls, _standing.CyclesSpent, MirrorBudget(), _standing.VoluntaryCyclesSpent, VoluntaryBudget(), cyclesThisSession, SessionCeiling(), flag, flag2, GuestPrewarmEnabled(), PhotonNetwork.isNonMasterClientInRoom); string text2 = GuestWarmPolicy.Reason(val2, cls); switch ((int)val2) { case 0: detail = ""; break; case 1: detail = (((int)cls == 1) ? $"{text2}: this room's voluntary donor-cycle budget is spent ({_standing.VoluntaryCyclesSpent}/{VoluntaryBudget()}; [Coop] GuestPrewarmBudget)" : $"{text2}: this room's cold-mirror donor-cycle budget is spent ({_standing.CyclesSpent}/{MirrorBudget()}; [Coop] MirrorHarvestBudget)"); break; case 2: detail = $"{text2}: this session's donor-cycle ceiling is reached ({cyclesThisSession}/{SessionCeiling()}; [Coop] GuestDonorCycleCeiling)"; break; case 3: detail = text2 + ": " + text; break; case 4: detail = text2 + ": the local player is in combat"; break; case 5: detail = text2 + ": [Coop] GuestPrewarm=false"; break; default: detail = text2; break; } return val2; } private static void TryWarmWanted() { //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_0203: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Invalid comparison between Unknown and I4 //IL_00ae: 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_0116: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_011d: 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_0135: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Invalid comparison between Unknown and I4 if (_standing.Warming || (Object)(object)Plugin.Instance == (Object)null || Time.unscaledTime < _nextWarmAttempt) { return; } _nextWarmAttempt = Time.unscaledTime + 5f; WantClass val = default(WantClass); string text = default(string); if (!_standing.TryPeekNext(ref text, ref val)) { return; } if (!PhotonNetwork.isNonMasterClientInRoom) { int num = _standing.ClearWanted((WantClass)0); if (num > 0) { Plugin.Log.LogMessage((object)$"[MIRROR] now master — dropped {num} host want(s); voluntary asks run on."); } if (_standing.TryTakeNext(ref text, ref val)) { if (SpawnTemplates.IsCached(text)) { FireVoluntaryDone(text, ok: true); return; } _standing.Warming = true; ((MonoBehaviour)Plugin.Instance).StartCoroutine(WarmRoutine(text, val, _standing.Generation)); } return; } string detail; GuestWarmVerdict val2 = Decide((WantClass)0, out detail); string detail2; GuestWarmVerdict val3 = Decide((WantClass)1, out detail2); int num2 = 0; text = null; string text2 = default(string); while (_standing.TryPeekAt(num2, ref text2, ref val)) { if (SpawnTemplates.IsCached(text2)) { _standing.RemoveAt(num2); FireVoluntaryDone(text2, ok: true); continue; } GuestWarmVerdict val4 = (((int)val == 1) ? val3 : val2); if ((int)val4 == 0) { text = text2; break; } if (!GuestWarmPolicy.IsTransient(val4)) { if ((int)val == 1) { _standing.RemoveAt(num2); Plugin.Log.LogWarning((object)("[TEMPLATE] voluntary warm '" + text2 + "' dropped — " + detail2 + ".")); FireVoluntaryDone(text2, ok: false); continue; } if (_voluntaryDone.ContainsKey(text2)) { Plugin.Log.LogWarning((object)("[TEMPLATE] prewarm '" + text2 + "' (the host's want) will not warm this room — " + detail + ".")); FireVoluntaryDone(text2, ok: false); } } num2++; } if (text != null) { _standing.RemoveAt(num2); _standing.Warming = true; ((MonoBehaviour)Plugin.Instance).StartCoroutine(WarmRoutine(text, val, _standing.Generation)); } } internal static void PrewarmVoluntary(string speciesKey, Action onDone) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Invalid comparison between Unknown and I4 //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Invalid comparison between Unknown and I4 //IL_00da: 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_010b: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Invalid comparison between Unknown and I4 //IL_011e: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) string text = speciesKey?.Trim() ?? ""; if (text.Length == 0) { onDone?.Invoke(obj: false); return; } if (SpawnTemplates.IsCached(text)) { Plugin.Log.LogMessage((object)("[TEMPLATE] prewarm '" + text + "' on a guest: already resident.")); onDone?.Invoke(obj: true); return; } WantOutcome val = _standing.Want(text, true, (WantClass)1); if ((int)val != 2) { if ((int)val == 3) { onDone?.Invoke(obj: false); return; } if (onDone != null) { if (!_voluntaryDone.TryGetValue(text, out var value)) { value = (_voluntaryDone[text] = new List>(1)); } value.Add(onDone); } WantClass val2 = default(WantClass); _standing.TryGetClass(text, ref val2); Decide(val2, out var detail); string text2 = VoluntaryCounterCore(); Plugin.Log.LogMessage((object)("[TEMPLATE] prewarm '" + text + "' on a guest: " + (((int)val == 1) ? "already queued" : "queued") + (((int)val2 == 0) ? " (as the host's want — its budget)" : (" (voluntary " + text2)) + (((int)val2 == 0) ? "" : ((detail.Length > 0) ? ("; waiting: " + detail + ")") : "; safe now)")) + ".")); _nextWarmAttempt = 0f; } else { Plugin.Log.LogWarning((object)("[TEMPLATE] prewarm '" + text + "' on a guest: refused — this machine already proved it cannot harvest that species this session (dead end).")); onDone?.Invoke(obj: false); } } private static void FireVoluntaryDone(string key, bool ok) { if (!_voluntaryDone.TryGetValue(key, out var value)) { return; } _voluntaryDone.Remove(key); foreach (Action item in value) { try { item(ok); } catch (Exception arg) { Plugin.Log.LogError((object)$"[TEMPLATE] prewarm callback for '{key}' threw: {arg}"); } } } private static void FailAllVoluntary(string why) { if (_voluntaryDone.Count == 0) { return; } List list = new List(_voluntaryDone.Keys); Plugin.Log.LogMessage((object)string.Format("[TEMPLATE] {0} pending voluntary warm(s) dropped ({1}): {2}.", list.Count, why, string.Join(", ", list.ToArray()))); foreach (string item in list) { FireVoluntaryDone(item, ok: false); } } internal static string VoluntaryStatus(string speciesKey) { //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) string text = speciesKey?.Trim() ?? ""; if (text.Length == 0) { return ""; } if (_warmingKey != null && string.Equals(_warmingKey, text, StringComparison.OrdinalIgnoreCase)) { return "warming now"; } WantClass cls = default(WantClass); if (!_standing.TryGetClass(text, ref cls)) { return ""; } Decide(cls, out var detail); int num = detail.IndexOf(':'); string text2 = ((num > 0) ? detail.Substring(0, num) : detail); if (detail.Length != 0) { return "queued to warm when safe (" + text2 + ")"; } return "queued to warm (next safe tick)"; } private static string VoluntaryCounterCore() { int num = VoluntaryBudget(); if (num > 0) { return $"{_standing.VoluntaryCyclesSpent}/{num}"; } return $"{_standing.VoluntaryCyclesSpent}/∞"; } internal static string VoluntaryCounter() { if (!PhotonNetwork.isNonMasterClientInRoom) { return ""; } return "(guest warm " + VoluntaryCounterCore() + ")"; } private static IEnumerator WarmRoutine(string speciesKey, WantClass cls, int generation) { //IL_000e: 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) try { _warmingKey = speciesKey; if ((int)cls == 1) { Plugin.Log.LogMessage((object)("[TEMPLATE] warming '" + speciesKey + "' (voluntary, this player's own ask) now that harvesting is safe.")); } else { Plugin.Log.LogMessage((object)("[MIRROR] warming refused cold species '" + speciesKey + "' now that harvesting is safe — the next sk.spawn for it mints from cache instead of stalling this machine.")); } _harvestsThisSession++; int cyclesBefore = DonorHarvest.CyclesThisSession; bool done = false; GameObject t = null; yield return SpawnTemplates.Acquire(speciesKey, delegate(GameObject x) { t = x; done = true; }, allowNonMaster: true); while (!done) { yield return null; } if (!_standing.IsCurrent(generation)) { Plugin.Log.LogMessage((object)("[MIRROR] warm '" + speciesKey + "' finished after a room change — result kept in the template cache, but not billed to this room's budget.")); if ((Object)(object)t != (Object)null) { WarmMirror.MarkDirty("warmed " + speciesKey + " (across a room change)"); } yield break; } _standing.ChargeCycles(DonorHarvest.CyclesThisSession - cyclesBefore, cls); if ((Object)(object)t == (Object)null) { _standing.MarkDeadEnd(speciesKey); } string arg = (PhotonNetwork.isNonMasterClientInRoom ? $"{_standing.CyclesSpent} donor cycle(s) spent on mirrors, {_standing.VoluntaryCyclesSpent} voluntary, {DonorHarvest.CyclesThisSession} this session" : $"run as master (budget-free), {DonorHarvest.CyclesThisSession} donor cycle(s) this session"); Plugin.Log.LogMessage((object)(string.Format("[MIRROR] warm '{0}' ({1}) {2} ", speciesKey, cls, ((Object)(object)t != (Object)null) ? "OK" : "FAILED (dead end — not retried this session)") + $"— {arg}, {_standing.WantedCount} species still wanted.")); FireVoluntaryDone(speciesKey, (Object)(object)t != (Object)null); WarmMirror.MarkDirty(((Object)(object)t != (Object)null) ? ("warmed " + speciesKey) : ("dead end " + speciesKey)); } finally { _warmingKey = null; _standing.ReleaseWarm(generation); } } private static int MirrorChainCap() { if (Plugin.MirrorDonorChainCap == null) { return 2; } return Plugin.MirrorDonorChainCap.Value; } private static WantOutcome WantSpecies(string speciesKey) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return WantSpecies(speciesKey, front: false); } internal static WantOutcome WantSpecies(string speciesKey, bool front) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) return _standing.Want(speciesKey, front); } internal static IReadOnlyList DeadEnds() { return _standing.DeadEnds(); } internal static int RemainingBudget() { return WarmSetWire.PublishedBudget(RemainingOf(MirrorBudget(), _standing.CyclesSpent), RemainingOf(SessionCeiling(), DonorHarvest.CyclesThisSession)); } private static int RemainingOf(int budget, int spent) { if (budget <= 0) { return -1; } int num = budget - spent; if (num <= 0) { return 0; } return num; } private static void TryProcessQueue() { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0108: 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) //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_0132: Expected I4, but got Unknown //IL_0290: Unknown result type (might be due to invalid IL or missing references) if (_queue.Count == 0) { return; } Character val = default(Character); bool flag = Lifecycle.TryGetFirstLocalCharacter(ref val); bool flag2 = (Object)(object)NetworkLevelLoader.Instance != (Object)null && NetworkLevelLoader.Instance.IsOverallLoadingDone; Scene activeScene = SceneManager.GetActiveScene(); string name = ((Scene)(ref activeScene)).name; float num = ((Plugin.MirrorQueueTimeoutSeconds != null) ? Plugin.MirrorQueueTimeoutSeconds.Value : 20f); bool flag3 = Plugin.GateColdMirrorHarvest == null || Plugin.GateColdMirrorHarvest.Value; bool flag4 = false; bool flag5 = false; string why = ""; for (int num2 = _queue.Count - 1; num2 >= 0; num2--) { PendingMirror pendingMirror = _queue[num2]; bool flag6 = !flag3 || SpawnTemplates.IsCached(pendingMirror.Msg.SpeciesKey); if (!flag6 && !flag4) { flag5 = HarvestSafeNow(out why); flag4 = true; } bool flag7 = flag6 || flag5; MirrorGate val2 = SpawnNetProtocol.DecideMirror(_replicas.ContainsKey(pendingMirror.Msg.Uid), flag, flag2, name, pendingMirror.Msg.Scene, Time.unscaledTime - pendingMirror.QueuedAt, num, flag6, flag7); switch ((int)val2) { case 0: _queue.RemoveAt(num2); if ((Object)(object)Plugin.Instance != (Object)null) { ((MonoBehaviour)Plugin.Instance).StartCoroutine(MirrorRoutine(pendingMirror)); } break; case 1: _queue.RemoveAt(num2); SpawnNet.CountDrop("sk.spawn", "dup"); break; case 2: _queue.RemoveAt(num2); SpawnNet.CountDrop("sk.spawn", "scene-timeout"); Plugin.Log.LogWarning((object)($"[MIRROR] uid={pendingMirror.Msg.Uid} timed out after {num:F0}s waiting ({pendingMirror.LastWait}) — " + "dropped + sk.fail(timeout). NB the master was streaming at an unknown viewID the whole wait (~10 warn+scene-scans/s on this machine) — [Coop] MirrorQueueTimeoutSeconds bounds that cost.")); SendFail(pendingMirror.Msg.Uid, "timeout"); break; case 5: _queue.RemoveAt(num2); SpawnNet.CountDrop("sk.spawn", "not-ready-timeout"); Plugin.Log.LogMessage((object)($"[MIRROR] uid={pendingMirror.Msg.Uid} dropped after {num:F0}s never-mintable " + "(" + pendingMirror.LastWait + ") — sk.fail(not-ready); the spawn STANDS on the master and our scene-ready resync re-asks for it (N-3).")); SendFail(pendingMirror.Msg.Uid, "not-ready"); break; case 6: _queue.RemoveAt(num2); SpawnNet.CountDrop("sk.spawn", "cold-unsafe"); WantSpecies(pendingMirror.Msg.SpeciesKey); Plugin.Log.LogMessage((object)("[MIRROR] refused cold species '" + pendingMirror.Msg.SpeciesKey + "' uid=" + pendingMirror.Msg.Uid + " — " + why + ". A cold mirror costs a full donor-scene load on this machine (seconds, not milliseconds), so the spawn is declined with sk.fail(cold-unsafe) rather than stalling " + $"us; queued to warm when safe ({_standing.WantedCount} species wanted).")); SendFail(pendingMirror.Msg.Uid, "cold-unsafe"); break; case 3: LogWaitOnce(pendingMirror, "player/loader not ready"); break; case 4: LogWaitOnce(pendingMirror, "scene mismatch (want '" + pendingMirror.Msg.Scene + "', have '" + name + "')"); break; } } } private static void LogWaitOnce(PendingMirror p, string wait) { if (!string.Equals(p.LastWait, wait, StringComparison.Ordinal)) { p.LastWait = wait; Plugin.Log.LogMessage((object)$"[MIRROR] queued uid={p.Msg.Uid} species='{p.Msg.SpeciesKey}' wait={wait} (queue={_queue.Count})."); } } private static IEnumerator MirrorRoutine(PendingMirror p) { string uid = p.Msg.Uid; _inFlight.Begin(uid); try { yield return MirrorRoutineBody(p); } finally { _inFlight.Finish(uid); } } private static IEnumerator MirrorRoutineBody(PendingMirror p) { SpawnMsg m = p.Msg; if (!PhotonNetwork.isNonMasterClientInRoom) { Plugin.Log.LogWarning((object)("[MIRROR] refuse mint uid=" + m.Uid + ": not a non-master client in a room (mirrors only exist on guests).")); SendFail(m.Uid, "not-guest"); yield break; } float t0 = Time.realtimeSinceStartup; int queueMs = (int)((Time.unscaledTime - p.QueuedAt) * 1000f); bool wasCached = SpawnTemplates.IsCached(m.SpeciesKey); if (!wasCached) { _harvestsThisSession++; Plugin.Log.LogMessage((object)("[MIRROR] cold species '" + m.SpeciesKey + "' — guest-local donor harvest starts (master-instructed; expect bounded 'no such view' warns until the mint lands).")); } GameObject template = null; bool acquired = false; int cyclesBefore = DonorHarvest.CyclesThisSession; int mintGeneration = _standing.Generation; IEnumerator enumerator = SpawnTemplates.Acquire(m.SpeciesKey, delegate(GameObject val4) { template = val4; acquired = true; }, allowNonMaster: true, (!wasCached) ? MirrorChainCap() : 0); if ((Object)(object)Plugin.Instance != (Object)null) { ((MonoBehaviour)Plugin.Instance).StartCoroutine(enumerator); } else { yield return enumerator; } float acquireLimit = ((Plugin.MirrorAcquireTimeoutSeconds != null) ? Plugin.MirrorAcquireTimeoutSeconds.Value : 25f); try { while (!acquired) { if (acquireLimit > 0f && Time.realtimeSinceStartup - t0 > acquireLimit) { Plugin.Log.LogWarning((object)("[MIRROR] uid=" + m.Uid + " abandoned: the template acquire for '" + m.SpeciesKey + "' " + $"passed {acquireLimit:F0}s ([Coop] MirrorAcquireTimeoutSeconds) and is still running. " + "sk.fail(timeout) sent; the harvest itself finishes in the background and its result is still cached, so a later spawn of this species mints instantly.")); SendFail(m.Uid, "timeout"); yield break; } yield return null; } } finally { if (_standing.IsCurrent(mintGeneration)) { _standing.ChargeCycles(DonorHarvest.CyclesThisSession - cyclesBefore); } } int acquireMs = (int)((Time.realtimeSinceStartup - t0) * 1000f); GoneKind val = default(GoneKind); if (_inFlight.TryPeek(m.Uid, ref val) && (int)val != 0) { Plugin.Log.LogMessage((object)($"[MIRROR] uid={m.Uid} was retired ({val}) during the {acquireMs}ms template " + "acquire — mint skipped entirely (A1-3 tombstone).")); yield break; } if (_replicas.ContainsKey(m.Uid)) { SpawnNet.CountDrop("sk.spawn", "dup"); Plugin.Log.LogMessage((object)("[MIRROR] duplicate mint uid=" + m.Uid + " lost the race — dropped (a concurrent routine already registered this uid; double-flush is expected traffic).")); yield break; } if ((Object)(object)template == (Object)null) { Plugin.Log.LogWarning((object)("[MIRROR] fail uid=" + m.Uid + ": no template for '" + m.SpeciesKey + "' on this machine (no additive donor and nothing in the expedition cache — expedition-only species can only mirror if this guest's cache holds a body). sk.fail sent.")); SendFail(m.Uid, "harvest"); yield break; } float realtimeSinceStartup = Time.realtimeSinceStartup; Replica r = Mint(template, m, wasCached ? "cache" : "harvest"); if (r == null) { SendFail(m.Uid, "mint"); yield break; } _replicas[r.Uid] = r; int mintMs = (int)((Time.realtimeSinceStartup - realtimeSinceStartup) * 1000f); float tDwell = Time.realtimeSinceStartup; yield return null; yield return null; int num = (int)((Time.realtimeSinceStartup - tDwell) * 1000f); Character character = r.Character; GameObject val2 = (((Object)(object)character != (Object)null) ? ((Component)character).gameObject : null); if ((Object)(object)val2 == (Object)null || !val2.activeInHierarchy) { Plugin.Log.LogWarning((object)("[MIRROR] uid=" + m.Uid + " went INACTIVE within two frames of activation — destroying (duplicate-UID guard signature would mean the master's uid collided locally; grep output_log.txt for 'has the same UID').")); if ((Object)(object)val2 != (Object)null) { Object.Destroy((Object)(object)val2); } _replicas.Remove(r.Uid); SendFail(m.Uid, "mint-inactive"); yield break; } bool flag = false; GoneKind val3 = default(GoneKind); if (_inFlight.TryPeek(m.Uid, ref val3)) { if ((int)val3 != 0) { Plugin.Log.LogMessage((object)($"[MIRROR] uid={m.Uid}: gone({val3}) arrived mid-mint — destroying the freshly " + "minted body instead of registering it (A1-3 tombstone).")); RemoveReplica(r, $"gone({val3}) mid-mint"); yield break; } flag = true; if (!character.IsDead) { Plugin.Log.LogMessage((object)("[MIRROR] uid=" + m.Uid + ": gone(died) arrived mid-mint — local Die() on the fresh replica (A1-3 tombstone; this is the self-heal the header promised).")); try { character.Die(Vector3.up, false); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[MIRROR] tombstone Die() threw: " + ex.Message)); } } r.State = ReplicaState.Dead; } float realtimeSinceStartup2 = Time.realtimeSinceStartup; bool flag2 = false; try { if (!character.IsRegisteredToManager) { CharacterManager.Instance.AddCharacter(character); } flag2 = character.IsRegisteredToManager; } catch (Exception ex2) { Plugin.Log.LogWarning((object)("[MIRROR] AddCharacter for uid=" + m.Uid + " threw: " + ex2.Message)); } int num2 = (int)((Time.realtimeSinceStartup - realtimeSinceStartup2) * 1000f); if (!flag) { r.State = ReplicaState.Active; r.ActiveAt = Time.unscaledTime; } int num3 = (int)((Time.realtimeSinceStartup - t0) * 1000f); string text = "?"; string text2 = "?"; string text3 = "?"; float num4 = -1f; try { text = (((Object)(object)character.Stats != (Object)null) ? $"{character.Stats.CurrentHealth:0.#}/{character.Stats.MaxHealth:0.#}" : "?"); } catch { } try { text2 = ((object)Unsafe.As(ref character.Faction)/*cast due to .constrained prefix*/).ToString(); } catch { } try { PhotonView component = val2.GetComponent(); text3 = ((component == null) ? "-" : (((Object)(object)component != (Object)null) ? component.group.ToString() : "")); } catch { } try { num4 = Vector3.Distance(val2.transform.position, new Vector3(m.X, m.Y, m.Z)); } catch { } Plugin.Log.LogMessage((object)(string.Format("[MIRROR] {0} uid={1} viewID={2} species='{3}' src={4} ", flag ? "corpse" : "active", r.Uid, r.ViewId, r.SpeciesKey, r.Source) + $"tookMs={num3} | registered={flag2} hp={text} faction={text2} " + $"pvGroup={text3} posDelta={num4:0.##} | timings=queue:{queueMs}|acquire:{acquireMs}|mint:{mintMs}|dwell:{num}|reg:{num2}ms")); if (!flag && Plugin.PostActivationVisualPass != null && Plugin.PostActivationVisualPass.Value) { VisualPass.Run(character, r.SpeciesKey); r.VisualPassAttempts = 1; } if (!flag) { CensusAndEnforce(r, "census"); } if (!flag2) { Plugin.Log.LogWarning((object)("[MIRROR] uid=" + r.Uid + " is NOT registered with CharacterManager — the 5s stale-stream pos resync and GetCharacter(uid) lookups will miss it (review g1).")); } SpawnNet.SendToMaster("sk.ack", SpawnNetProtocol.EncodeAck(new AckMsg { Uid = r.Uid, Source = r.Source, TookMs = num3 })); if (!flag) { Spawner.RaiseMirrored(InfoFor(r)); } } private static Replica Mint(GameObject template, SpawnMsg m, string source) { //IL_026e: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Invalid comparison between Unknown and I4 //IL_0114: 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_013b: Unknown result type (might be due to invalid IL or missing references) //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_0155: 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_016f: 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_0192: Unknown result type (might be due to invalid IL or missing references) //IL_0198: Unknown result type (might be due to invalid IL or missing references) //IL_019e: Unknown result type (might be due to invalid IL or missing references) //IL_01a8: Unknown result type (might be due to invalid IL or missing references) //IL_01b3: Unknown result type (might be due to invalid IL or missing references) //IL_01d0: Unknown result type (might be due to invalid IL or missing references) //IL_01f3: Unknown result type (might be due to invalid IL or missing references) //IL_01ff: Unknown result type (might be due to invalid IL or missing references) //IL_0220: Unknown result type (might be due to invalid IL or missing references) //IL_0104: 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_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_0235: Unknown result type (might be due to invalid IL or missing references) //IL_024a: Unknown result type (might be due to invalid IL or missing references) GameObject val = Object.Instantiate(template, Holder().transform); try { ((Object)val).name = ((Object)template).name.Replace("SK_Template_", "SK_Replica_"); Character component = val.GetComponent(); if ((Object)(object)component == (Object)null) { Plugin.Log.LogError((object)"[MIRROR] template clone has no Character component — destroying."); Object.Destroy((Object)(object)val); return null; } ShapeCensus val2 = ReplicaDrive.Census(val); ShapePlan val3 = ReplicaShape.DecideTemplateClone(val2); Plugin.Log.LogMessage((object)("[MIRROR] mint shape uid=" + m.Uid + ": " + ReplicaShape.Describe(val2))); if ((int)val3.Verdict != 0) { if ((int)val3.Verdict == 2) { Plugin.Log.LogWarning((object)("[MIRROR] CORRUPT-TEMPLATE uid=" + m.Uid + ": the template clone is a converted-donor replica (no CharacterAI) — the [HARVEST-GUARD] NetworkInit suppression did NOT cover this template's harvest; rebuilding the stream drive by hand while inactive.")); } string text = ReplicaDrive.Apply(val, val3); Plugin.Log.LogMessage((object)("[MIRROR] mint scrub uid=" + m.Uid + ": " + text)); } SpawnOptions spawnOptions = new SpawnOptions { StripQuestEvents = m.StripQuestEvents }; if (m.Faction >= 0) { spawnOptions.Faction = (Factions)m.Faction; } int viewId = m.ViewId; MintNormalize.Apply(val, component, spawnOptions, out var uid, ref viewId, new MintNormalize.MintIdentity { Uid = m.Uid, ViewId = m.ViewId, RightHandItemId = m.RightHandItemId, RightHandItemUid = m.RightHandItemUid, LeftHandItemId = m.LeftHandItemId, LeftHandItemUid = m.LeftHandItemUid }); val.transform.SetPositionAndRotation(new Vector3(m.X, m.Y, m.Z), Quaternion.Euler(0f, m.YawDeg, 0f)); val.transform.SetParent((Transform)null, true); MintNormalize.DeferAiCulling(val, "uid=" + m.Uid + " (mirror mint)"); return new Replica { Uid = uid, ViewId = m.ViewId, SpeciesKey = m.SpeciesKey, Character = component, State = ReplicaState.Mirroring, Source = source, ConsumerData = (m.ConsumerData ?? ""), ShippedRhUid = (m.RightHandItemUid ?? ""), ShippedLhUid = (m.LeftHandItemUid ?? "") }; } catch (Exception arg) { Plugin.Log.LogError((object)$"[MIRROR] mint of uid={m.Uid} threw — destroying pending clone: {arg}"); if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } return null; } } private static string B(bool b) { if (!b) { return "F"; } return "T"; } private static string SyncStr(ViewSyncMode m) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected I4, but got Unknown return (int)m switch { 1 => "Off", 2 => "Unreliable", 0 => "Unknown", _ => "Other", }; } private static GateEval ReadGates(Character c, in ShapeCensus census, out string desc) { //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_01eb: Unknown result type (might be due to invalid IL or missing references) //IL_0211: Unknown result type (might be due to invalid IL or missing references) //IL_022a: Unknown result type (might be due to invalid IL or missing references) //IL_0246: Unknown result type (might be due to invalid IL or missing references) bool flag = false; bool b = false; bool flag2 = false; bool flag3 = false; bool b2 = false; bool b3 = false; bool b4 = false; string text = "?"; try { flag = c.Initialized; } catch { } try { b = c.Alive; } catch { } try { flag2 = c.IsAI; } catch { } try { flag3 = c.SendInitDone; } catch { } try { b2 = c.m_startInitDone; } catch { } try { b3 = c.m_lateInitDone; } catch { } try { b4 = c.m_equipmentInit; } catch { } try { text = (Time.time - c.TimeOfLastSerialize).ToString("0.0") + "s"; } catch { } bool flag4 = (Object)(object)NetworkLevelLoader.Instance != (Object)null && NetworkLevelLoader.Instance.IsOverallLoadingDone; GateEval val = ReplicaShape.EvalGates(flag2, flag3, flag4, flag, census.CloseToPlayer, census.CcEnabled, census.SyncMode); string text2 = (val.OuterGate ? "T" : ("BLOCKED(isAI=" + B(flag2) + " sendInit=" + B(flag3) + " loadDone=" + B(flag4) + ")")); desc = "init=" + B(flag) + "(startInit=" + B(b2) + " lateInit=" + B(b3) + " equipInit=" + B(b4) + ") alive=" + B(b) + " isAI=" + B(flag2) + " sendInit=" + B(flag3) + " loadDone=" + B(flag4) + " serAge=" + text + " sync=" + SyncStr(census.SyncMode) + " outerGate=" + text2 + " gateFull=" + B(val.FullGate) + "(syncOk=" + B(val.SyncOk) + ")"; return val; } private static string DescribeGates(Character c, in ShapeCensus census) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) ReadGates(c, in census, out var desc); return desc; } private static bool CensusAndEnforce(Replica r, string tag) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: 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) //IL_00c7: 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_02c2: Unknown result type (might be due to invalid IL or missing references) //IL_01e1: Unknown result type (might be due to invalid IL or missing references) //IL_01e6: Unknown result type (might be due to invalid IL or missing references) //IL_01e8: Unknown result type (might be due to invalid IL or missing references) //IL_0368: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Unknown result type (might be due to invalid IL or missing references) Character character = r.Character; if ((Object)(object)character == (Object)null || (Object)(object)((Component)character).gameObject == (Object)null) { return false; } ShapeCensus census = ReplicaDrive.Census(((Component)character).gameObject); ShapePlan val = ReplicaShape.DecideActiveReplica(census); string text = (((int)val.Fixes == 0) ? "none" : ReplicaDrive.Apply(((Component)character).gameObject, val)); bool flag = false; if (Plugin.GuestEquipWeapon != null && Plugin.GuestEquipWeapon.Value) { int num = HumanoidWeapon.EnsureEquipped(character); if (num > 0) { r.WeaponForced += num; } } if (Plugin.PostActivationVisualPass != null && Plugin.PostActivationVisualPass.Value && r.VisualPassAttempts > 0 && !r.VisualConvergedLogged) { Census val2 = VisualPass.Measure(((Component)character).gameObject); string arg = default(string); Action val3 = VisualGate.Decide(ref val2, ref arg); if ((int)val3 == 0) { r.VisualConvergedLogged = true; Plugin.Log.LogMessage((object)($"[MIRROR] visual converged uid={r.Uid} after {r.VisualPassAttempts} pass(es) " + $"(+{Time.unscaledTime - r.ActiveAt:0.0}s).")); } else if (r.VisualPassAttempts < 3) { r.VisualPassAttempts++; VisualPass.Run(character, r.SpeciesKey); flag = true; } else { r.VisualConvergedLogged = true; Plugin.Log.LogWarning((object)($"[MIRROR] visual NOT converged uid={r.Uid} after {r.VisualPassAttempts} passes — " + $"gate still {val3} ({arg}). Leaving it (Rebind is not idempotent); read the last " + "[SPAWN] visual diag segment for the H1/H2/H3 discriminators.")); } } string text2 = ReplicaDrive.NudgeInit(character, r.InitNudgeAttempts) ?? "none"; if (text2 != "none") { r.InitNudgeAttempts++; } else if (r.InitNudgeAttempts > 0 && !r.InitConvergedLogged) { string desc; GateEval val4 = ReadGates(character, in census, out desc); if (val4.ConvergedOk) { r.InitConvergedLogged = true; Plugin.Log.LogMessage((object)($"[MIRROR] init converged uid={r.Uid} after {r.InitNudgeAttempts} nudge attempt(s), " + $"+{Time.unscaledTime - r.ActiveAt:0.0}s — {desc}")); } else if (!r.InitBlockedLogged) { r.InitBlockedLogged = true; Plugin.Log.LogWarning((object)("[MIRROR] init NOT converged uid=" + r.Uid + ": the equipment nudge is done but the drive gate is still closed — " + desc + ". gateFull=T cannot be claimed until outerGate holds and the view is Unreliable (B1); the watch keeps enforcing sync/shape.")); } } if (tag == "census") { Plugin.Log.LogMessage((object)("[MIRROR] census uid=" + r.Uid + ": " + ReplicaShape.Describe(census) + " " + DescribeGates(character, in census) + " | enforce: " + text + " initNudge: " + text2 + " | " + WeaponCensus(r, character) + " | " + TiltCensus(character))); } else { if (text != "none") { r.EnforceRepeats++; Plugin.Log.LogWarning((object)($"[MIRROR] enforce uid={r.Uid} corrected: {text} (repeat #{r.EnforceRepeats}; " + ReplicaShape.Describe(census) + ") — repeating corrections mean something re-flips the drive state.")); } if (text2 != "none") { Plugin.Log.LogMessage((object)($"[MIRROR] init nudge uid={r.Uid}: {text2} (attempt {r.InitNudgeAttempts}) — " + DescribeGates(character, in census))); } } return text != "none" || text2 != "none" || flag; } private static string WeaponCensus(Replica r, Character c) { string text = "none"; bool flag = false; try { Weapon currentWeapon = c.CurrentWeapon; if ((Object)(object)currentWeapon != (Object)null) { text = ((Item)currentWeapon).UID; flag = true; } } catch { } string text2 = ((string.IsNullOrEmpty(r.ShippedRhUid) && string.IsNullOrEmpty(r.ShippedLhUid)) ? "-" : (flag ? ((string.Equals(text, r.ShippedRhUid, StringComparison.Ordinal) || string.Equals(text, r.ShippedLhUid, StringComparison.Ordinal)) ? "T" : "F") : "F")); string text3 = ((r.WeaponForced > 0) ? "force" : (flag ? "vanilla" : "none")); return "weapon: " + HumanoidWeapon.StateFor(c) + " wpnUid=" + text + " wpnUidMatch=" + text2 + " equipSrc=" + text3; } private static string TiltCensus(Character c) { //IL_000b: 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) try { return $"tilt: fwdY={((Component)c).transform.forward.y:0.##} wantedFwdY={c.WantedForward.y:0.##}"; } catch { return "tilt: ?"; } } internal static string SetFollow(string arg) { if (string.IsNullOrEmpty(arg) || string.Equals(arg, "off", StringComparison.OrdinalIgnoreCase)) { string followUid = _followUid; _followUid = null; if (followUid != null) { return "[MIRROR] skfollow OFF (was " + followUid + ")."; } return "[MIRROR] skfollow: was not armed."; } foreach (Replica value in _replicas.Values) { if (value.Uid.IndexOf(arg, StringComparison.OrdinalIgnoreCase) >= 0) { _followUid = value.Uid; return "[MIRROR] skfollow ON for uid=" + value.Uid + " — hard-snapping to the stream every frame (NCC bypassed)."; } } return "[MIRROR] skfollow: no replica matches '" + arg + "' (skcoopdump lists them)."; } private static void FollowTick() { //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006d: 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) if (_followUid == null) { return; } if (!_replicas.TryGetValue(_followUid, out var value) || (Object)(object)value.Character == (Object)null) { Plugin.Log.LogMessage((object)("[MIRROR] skfollow: replica " + _followUid + " is gone — disarmed.")); _followUid = null; return; } try { Character character = value.Character; ((Component)character).transform.position = character.WantedPosition; Vector3 wantedForward = character.WantedForward; if (((Vector3)(ref wantedForward)).sqrMagnitude > 0.001f) { ((Component)character).transform.forward = wantedForward; } Physics.SyncTransforms(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[MIRROR] skfollow snap threw — disarmed: " + ex.Message)); _followUid = null; } } private static void WatchTick() { //IL_010c: 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_0113: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Invalid comparison between Unknown and I4 //IL_014c: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Invalid comparison between Unknown and I4 if (_replicas.Count == 0) { return; } _watchSnapshot.Clear(); foreach (Replica value in _replicas.Values) { _watchSnapshot.Add(value); } foreach (Replica item in _watchSnapshot) { if (item.State == ReplicaState.Mirroring) { continue; } bool flag = (Object)(object)item.Character != (Object)null; bool flag2 = flag && EnemySpawner.SafeAlive(item.Character); if (item.State == ReplicaState.Active) { if (flag2 && Time.unscaledTime >= item.NextEnforceAt) { bool flag3 = CensusAndEnforce(item, "enforce"); item.EnforceInterval = (flag3 ? 0f : Mathf.Min(Mathf.Max(item.EnforceInterval * 2f, 0.5f), 4f)); item.NextEnforceAt = Time.unscaledTime + item.EnforceInterval; } WatchState val = SpawnWatch.Next((WatchState)1, flag, flag2); if ((int)val == 2) { item.State = ReplicaState.Dead; SpawnDisengage.DisengageSpawn((RemovalReason)2, item.Character); Plugin.Log.LogMessage((object)("[MIRROR] uid=" + item.Uid + " died (stream/death-RPC) — corpse stays, guest disengage swept.")); } else if ((int)val == 3) { RemoveReplica(item, "vanished"); } } else if (item.State == ReplicaState.Dead && !flag) { RemoveReplica(item, "corpse-gone"); } } } private static void RemoveReplica(Replica r, string why) { _replicas.Remove(r.Uid); Character character = r.Character; SpawnDisengage.DisengageSpawn((RemovalReason)((!((Object)(object)character != (Object)null)) ? 3 : 0), character); if ((Object)(object)character != (Object)null && (Object)(object)((Component)character).gameObject != (Object)null) { Object.Destroy((Object)(object)((Component)character).gameObject); } Plugin.Log.LogMessage((object)$"[MIRROR] gone uid={r.Uid} action=removed ({why}); {_replicas.Count} replica(s) remain."); } private static void SendFail(string uid, string reason) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) SpawnNet.SendToMaster("sk.fail", SpawnNetProtocol.EncodeFail(new FailMsg { Uid = uid, Reason = reason })); } internal static void OnRoomChanged() { _standing.Reset(); FailAllVoluntary("room changed"); if (_replicas.Count == 0 && _queue.Count == 0 && _inFlight.InFlightCount == 0) { return; } Plugin.Log.LogMessage((object)$"[MIRROR] room changed — clearing {_replicas.Count} replica(s) + {_queue.Count} queued."); List list = new List(_replicas.Values); foreach (Replica item in list) { RemoveReplica(item, "room-change"); } _queue.Clear(); _inFlight.Clear(); } internal static bool ForceDrop(string uid) { if (!_replicas.TryGetValue(uid, out var value)) { return false; } Plugin.Log.LogMessage((object)("[MIRROR] skdrop uid=" + uid + " — destroying the replica, master NOT told (loss-race staging).")); RemoveReplica(value, "skdrop"); return true; } internal static string StreamDump(string uidFilter) { //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_01d3: Unknown result type (might be due to invalid IL or missing references) if (_replicas.Count == 0) { return "[MIRROR] no replicas."; } StringBuilder stringBuilder = new StringBuilder("[MIRROR] stream health:"); foreach (Replica value in _replicas.Values) { if (!string.IsNullOrEmpty(uidFilter) && value.Uid.IndexOf(uidFilter, StringComparison.OrdinalIgnoreCase) < 0) { continue; } Character character = value.Character; if ((Object)(object)character == (Object)null) { stringBuilder.Append("\n uid=" + value.Uid + " "); continue; } string text = "?"; string text2 = "?"; float num = -1f; try { text = (Time.time - character.TimeOfLastSerialize).ToString("0.0") + "s"; } catch { } try { text2 = (((Object)(object)character.Stats != (Object)null) ? $"{character.Stats.CurrentHealth:0.#}/{character.Stats.MaxHealth:0.#}" : "?"); } catch { } try { num = Vector3.Distance(((Component)character).transform.position, character.WantedPosition); } catch { } ShapeCensus census = ReplicaDrive.Census(((Component)character).gameObject); stringBuilder.Append($"\n uid={value.Uid} state={value.State} serializeAge={text} hp={text2} posDelta={num:0.##} " + $"active={((Component)character).gameObject.activeInHierarchy} dead={character.IsDead} enforceRepeats={value.EnforceRepeats}" + ((_followUid == value.Uid) ? " FOLLOW" : "")); stringBuilder.Append("\n shape: " + ReplicaShape.Describe(census) + " " + DescribeGates(character, in census)); stringBuilder.Append("\n " + WeaponCensus(value, character)); } return stringBuilder.ToString(); } private static ReplicaInfo InfoFor(Replica r) { return new ReplicaInfo { Uid = r.Uid, SpeciesKey = r.SpeciesKey, State = (MirrorState)r.State, ViewId = r.ViewId, ConsumerData = (r.ConsumerData ?? "") }; } internal static IReadOnlyList SnapshotInfos() { List list = new List(_replicas.Count); foreach (Replica value in _replicas.Values) { list.Add(InfoFor(value)); } return list; } internal static string Dump() { string why; string why2; StringBuilder stringBuilder = new StringBuilder($"[MIRROR] {_replicas.Count} replica(s), {_queue.Count} queued, " + $"{_harvestsThisSession} guest harvest attempt(s) this session" + $" (mirror {_standing.CyclesSpent}/{BudgetLabel(MirrorBudget())} + voluntary {_standing.VoluntaryCyclesSpent}/{BudgetLabel(VoluntaryBudget())} donor cycles spent, " + $"session {DonorHarvest.CyclesThisSession}/{BudgetLabel(SessionCeiling())} ceiling, published budget={RemainingBudget()}, " + string.Format("safe-now(host)={0}{1}, ", HarvestSafeNow(out why), string.IsNullOrEmpty(why) ? "" : (": " + why)) + string.Format("safe-now(voluntary)={0}{1}, ", HarvestSafeNow((WantClass)1, out why2), string.IsNullOrEmpty(why2) ? "" : (": " + why2)) + "wanted=[" + WantedDump() + "] deadEnds=[" + string.Join(", ", _standing.DeadEnds().ToArray()) + "]):"); foreach (Replica value in _replicas.Values) { Character character = value.Character; string text = (((Object)(object)character == (Object)null) ? "" : $"alive={EnemySpawner.SafeAlive(character)} active={((Component)character).gameObject.activeInHierarchy} registered={SafeRegistered(character)}"); stringBuilder.Append($"\n uid={value.Uid} viewID={value.ViewId} species='{value.SpeciesKey}' lifecycle={value.State} src={value.Source} {text}" + (string.IsNullOrEmpty(value.ConsumerData) ? "" : (" consumerData='" + value.ConsumerData + "'"))); } foreach (PendingMirror item in _queue) { stringBuilder.Append("\n QUEUED uid=" + item.Msg.Uid + " species='" + item.Msg.SpeciesKey + "' scene='" + item.Msg.Scene + "' " + $"for={Time.unscaledTime - item.QueuedAt:F0}s wait={item.LastWait}"); } return stringBuilder.ToString(); } private static string BudgetLabel(int b) { if (b > 0) { return b.ToString(); } return "∞"; } internal static string BucketsDump() { return $"mirror {_standing.CyclesSpent}/{BudgetLabel(MirrorBudget())} voluntary {_standing.VoluntaryCyclesSpent}/{BudgetLabel(VoluntaryBudget())} " + $"session {DonorHarvest.CyclesThisSession}/{BudgetLabel(SessionCeiling())} " + "guestPrewarm=" + (GuestPrewarmEnabled() ? "on" : "off"); } internal static string WantedDump() { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Invalid comparison between Unknown and I4 StringBuilder stringBuilder = new StringBuilder(); foreach (KeyValuePair item in _standing.WantedWithClass()) { if (stringBuilder.Length > 0) { stringBuilder.Append(", "); } stringBuilder.Append(item.Key).Append(((int)item.Value == 1) ? "(voluntary)" : "(host)"); } return stringBuilder.ToString(); } private static bool SafeRegistered(Character c) { try { return c.IsRegisteredToManager; } catch { return false; } } } internal static class HumanoidWeapon { private static readonly EquipmentSlotIDs[] HandSlots = (EquipmentSlotIDs[])(object)new EquipmentSlotIDs[2] { (EquipmentSlotIDs)5, (EquipmentSlotIDs)6 }; internal static int EnsureEquipped(Character ch) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected I4, but got Unknown //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: 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) int num = 0; try { CharacterEquipment val = (((Object)(object)ch != (Object)null && (Object)(object)ch.Inventory != (Object)null) ? ch.Inventory.Equipment : null); EquipmentSlot[] array = (((Object)(object)val != (Object)null) ? val.EquipmentSlots : null); if (array == null) { return 0; } EquipmentSlotIDs[] handSlots = HandSlots; foreach (EquipmentSlotIDs val2 in handSlots) { int num2 = (int)val2; if (num2 < 0 || num2 >= array.Length) { continue; } EquipmentSlot val3 = array[num2]; if (!((Object)(object)val3 == (Object)null) && !val3.HasItemEquipped) { Item componentInChildren = ((Component)val3).GetComponentInChildren(true); Equipment val4 = (Equipment)(object)((componentInChildren is Equipment) ? componentInChildren : null); if (!((Object)(object)val4 == (Object)null) && val4.EquipSlot == val2) { val.ForceEquipItem(val4); num++; Plugin.Log.LogMessage((object)($"[SPAWN] re-equipped {val2} '{((Item)val4).Name}' on '{((Object)((Component)ch).gameObject).name}' " + "(Bug 39 humanoid weapon-draw fix — CurrentWeapon was null; StartingEquipment.Init never re-fires on a mid-gameplay clone).")); } } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SPAWN] hand-weapon re-equip threw: " + ex.Message)); } return num; } internal static void HandIdentity(Character ch, EquipmentSlotIDs id, out int itemId, out string uid) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected I4, but got Unknown //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) itemId = 0; uid = ""; try { CharacterEquipment val = (((Object)(object)ch != (Object)null && (Object)(object)ch.Inventory != (Object)null) ? ch.Inventory.Equipment : null); EquipmentSlot[] array = (((Object)(object)val != (Object)null) ? val.EquipmentSlots : null); int num = (int)id; if (array == null || num < 0 || num >= array.Length || (Object)(object)array[num] == (Object)null) { return; } EquipmentSlot val2 = array[num]; Equipment val3 = (val2.HasItemEquipped ? val2.EquippedItem : null); if ((Object)(object)val3 == (Object)null) { Item componentInChildren = ((Component)val2).GetComponentInChildren(true); Equipment val4 = (Equipment)(object)((componentInChildren is Equipment) ? componentInChildren : null); if ((Object)(object)val4 != (Object)null && val4.EquipSlot == id) { val3 = val4; } } if (!((Object)(object)val3 == (Object)null)) { itemId = ((Item)val3).ItemID; uid = ((Item)val3).UID ?? ""; } } catch { itemId = 0; uid = ""; } } internal static string StateFor(Character ch) { string text = "none"; string text2 = "?"; string text3 = "-"; try { text = (((Object)(object)ch.CurrentWeapon != (Object)null) ? ((Item)ch.CurrentWeapon).Name : "none"); } catch { } try { text2 = ch.Sheathed.ToString(); } catch { } try { CharacterAI component = ((Component)ch).GetComponent(); if ((Object)(object)component != (Object)null && component.AiStates != null) { AIState[] aiStates = component.AiStates; foreach (AIState val in aiStates) { AISCombatMelee val2 = (AISCombatMelee)(object)((val is AISCombatMelee) ? val : null); if (val2 != null) { text3 = ((val2.AttackPatterns != null) ? val2.AttackPatterns.Length : 0).ToString(); break; } } } } catch { } return "weapon='" + text + "' sheathed=" + text2 + " atkPatterns=" + text3; } } internal static class LootProbe { internal static CorpseLootObservation Observe(Character ch) { //IL_0171: Unknown result type (might be due to invalid IL or missing references) bool flag = true; try { flag = !ch.Alive; } catch { } LootableOnDeath val = null; try { val = ((Component)ch).GetComponent(); } catch { } bool flag2 = (Object)(object)val != (Object)null; bool flag3 = false; if (flag2) { try { flag3 = ((Behaviour)val).enabled && ((Component)val).gameObject.activeInHierarchy; } catch { } } int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; bool flag4 = false; bool flag5 = false; if (flag2) { try { DropInstance[] lootDrops = val.LootDrops; if (lootDrops != null) { num = lootDrops.Length; DropInstance[] array = lootDrops; foreach (DropInstance val2 in array) { if (val2 != null && (Object)(object)val2.Dropper != (Object)null) { num2++; } } } } catch { } try { DropInstance[] skinDrops = val.SkinDrops; if (skinDrops != null) { num3 = skinDrops.Length; DropInstance[] array2 = skinDrops; foreach (DropInstance val3 in array2) { if (val3 != null && (Object)(object)val3.Dropper != (Object)null) { num4++; } } } } catch { } try { flag4 = val.m_lootable; } catch { } try { flag5 = val.m_skinable; } catch { } } ItemContainer val4 = null; try { val4 = (((Object)(object)ch.Inventory != (Object)null) ? ch.Inventory.Pouch : null); } catch { } bool flag6 = (Object)(object)val4 != (Object)null; bool flag7 = false; if (flag6) { try { flag7 = ((Item)val4).HasInteractionTrigger; } catch { } } return new CorpseLootObservation(flag, flag2, flag3, flag6, flag7, num, num2, num3, num4, flag4, flag5); } internal static void Log(Character ch, string context) { //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Unknown result type (might be due to invalid IL or missing references) //IL_0128: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0153: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_018f: Unknown result type (might be due to invalid IL or missing references) //IL_019d: Unknown result type (might be due to invalid IL or missing references) //IL_01ab: Unknown result type (might be due to invalid IL or missing references) //IL_01c4: Unknown result type (might be due to invalid IL or missing references) //IL_01cf: Unknown result type (might be due to invalid IL or missing references) //IL_01e7: Unknown result type (might be due to invalid IL or missing references) //IL_01f5: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) if (Plugin.Log == null) { return; } if ((Object)(object)ch == (Object)null) { Plugin.Log.LogMessage((object)("[LOOT] " + context + ": character is null (already destroyed).")); return; } CorpseLootObservation val; try { val = Observe(ch); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LOOT] " + context + ": probe threw: " + ex.Message)); return; } CorpseLootVerdict val2 = CorpseLootDiagnosis.Classify(ref val); string text = "?"; int num = -1; bool flag = false; bool flag2 = false; bool flag3 = false; try { text = ((Object)((Component)ch).gameObject).name; } catch { } try { flag3 = ch.HasEventOnDeath; } catch { } try { ItemContainer val3 = (((Object)(object)ch.Inventory != (Object)null) ? ch.Inventory.Pouch : null); if ((Object)(object)val3 != (Object)null) { try { num = val3.ItemCount; } catch { } try { flag = ((Item)val3).IsForceInteractible; } catch { } try { flag2 = val3.VisibleIfEmpty; } catch { } } } catch { } Plugin.Log.LogMessage((object)($"[LOOT] {context}: '{text}' dead={val.IsDead} verdict={val2} — {CorpseLootDiagnosis.Explain(val2)}" + (CorpseLootDiagnosis.IsActionableDefect(val2) ? " [ACTIONABLE]" : "") + $" | LootableOnDeath={(val.HasComponent ? 1 : 0)} enabled={val.ComponentEnabled}" + $" | lootDrops={val.LootDropsWithDropper}/{val.LootDropEntries} skinDrops={val.SkinDropsWithDropper}/{val.SkinDropEntries}" + $" m_lootable={val.Lootable} m_skinable={val.Skinable}" + $" | pouch={(val.HasPouch ? 1 : 0)} trigger={val.PouchHasInteractionTrigger} items={num}" + $" forceInteractible={flag} visibleIfEmpty={flag2} hasEventOnDeath={flag3}")); } } internal static class MintNormalize { internal struct MintIdentity { public string Uid; public int ViewId; public int RightHandItemId; public string RightHandItemUid; public int LeftHandItemId; public string LeftHandItemUid; } internal const float AiCullGraceSeconds = 1f; internal static void Apply(GameObject go, Character ch, SpawnOptions opts, out string uid, ref int viewId, MintIdentity? identity = null) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_057f: Unknown result type (might be due to invalid IL or missing references) //IL_0584: Unknown result type (might be due to invalid IL or missing references) //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_014a: Invalid comparison between Unknown and I4 //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_0156: Invalid comparison between Unknown and I4 //IL_01ba: 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_01e3: Unknown result type (might be due to invalid IL or missing references) uid = (identity.HasValue ? identity.Value.Uid : SpawnUid.Mint(Guid.NewGuid())); ch.SetUID(UID.op_Implicit(uid)); ch.NonSavable = true; if (identity.HasValue) { if (!EnemySpawner.Lease.Bind(go, identity.Value.ViewId, "mirror mint")) { throw new InvalidOperationException("mirror mint: template clone has no PhotonView — cannot bind the shared viewID."); } viewId = identity.Value.ViewId; } else { int num = EnemySpawner.Lease.Mint(go, "master mint"); if (num >= 0) { viewId = num; } else { Plugin.Log.LogWarning((object)("[SPAWN] mint uid=" + uid + " '" + ((Object)go).name + "' has NO PhotonView — viewID stays -1: spawn will NOT replicate to guests (local-only creature). Fine in solo play; in a co-op session this template lost its view — investigate.")); } } Item val = null; Item val2 = null; string text = (identity.HasValue ? identity.Value.RightHandItemUid : null); string text2 = (identity.HasValue ? identity.Value.LeftHandItemUid : null); if (!string.IsNullOrEmpty(text) || !string.IsNullOrEmpty(text2)) { try { EquipmentSlot[] componentsInChildren = go.GetComponentsInChildren(true); foreach (EquipmentSlot val3 in componentsInChildren) { if ((Object)(object)val3 == (Object)null) { continue; } bool flag = (int)val3.SlotType == 5; bool flag2 = (int)val3.SlotType == 6; if (!flag && !flag2) { continue; } string value = (flag ? text : text2); if (string.IsNullOrEmpty(value)) { continue; } int num2 = (flag ? identity.Value.RightHandItemId : identity.Value.LeftHandItemId); Item componentInChildren = ((Component)val3).GetComponentInChildren(true); Equipment val4 = (Equipment)(object)((componentInChildren is Equipment) ? componentInChildren : null); if (!((Object)(object)val4 == (Object)null) && val4.EquipSlot == val3.SlotType) { if (num2 != 0 && ((Item)val4).ItemID != num2) { Plugin.Log.LogWarning((object)($"[MIRROR] hand-identity MISMATCH {val3.SlotType}: master shipped " + $"ItemID {num2} but this machine's template holds {((Item)val4).ItemID} ('{((Item)val4).Name}') — keeping a " + "fresh local UID (adopting would aim the master's item-sync at a different item). Divergent donor resolution between the two machines; expect equipSrc=force to carry this one.")); } else if (flag) { val = (Item)(object)val4; } else { val2 = (Item)(object)val4; } } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[MIRROR] hand-identity resolution threw: " + ex.Message)); } } int num3 = 0; int num4 = 0; Item[] componentsInChildren2 = go.GetComponentsInChildren(true); foreach (Item val5 in componentsInChildren2) { if ((Object)(object)val5 == (Object)null) { continue; } try { if ((Object)(object)val5 == (Object)(object)val) { val5.UID = text; num4++; } else if ((Object)(object)val5 == (Object)(object)val2) { val5.UID = text2; num4++; } else { val5.UID = SpawnUid.MintItem(Guid.NewGuid()); } num3++; } catch (Exception ex2) { Plugin.Log.LogWarning((object)("[SPAWN] re-UID of a child item threw: " + ex2.Message)); } } Plugin.Log.LogMessage((object)($"[SPAWN] re-UID'd {num3} child item(s) with fresh SKi_ UIDs (SK-A weapon-equip fix)." + ((num4 > 0) ? string.Format(" Adopted {0} master hand-slot identit{1} (MP §1 guest weapon-sync fix).", num4, (num4 == 1) ? "y" : "ies") : ""))); if (opts.StripQuestEvents) { QuestEventOnDeath[] componentsInChildren3 = go.GetComponentsInChildren(true); QuestEventOnDeath[] array = componentsInChildren3; foreach (QuestEventOnDeath val6 in array) { if ((Object)(object)val6 != (Object)null) { Object.DestroyImmediate((Object)(object)val6); } } int num5 = 0; QuestEventOnDeath[] componentsInChildren4 = go.GetComponentsInChildren(true); foreach (QuestEventOnDeath val7 in componentsInChildren4) { if ((Object)(object)val7 != (Object)null) { num5++; } } if (num5 > 0) { Plugin.Log.LogError((object)("[SPAWN] QuestEventOnDeath strip was a SILENT NO-OP on '" + ((Object)go).name + "': " + $"{num5} of {componentsInChildren3.Length} survived DestroyImmediate. Two possible causes, and " + "the fix differs: EITHER this mint ran inside an animation event, a physics trigger/contact callback, a StateMachineBehaviour callback or a render callback, where Unity refuses immediate destruction (logs, returns, never throws) — move the mint call site back onto Update/a coroutine; OR a [RequireComponent] protection on this donor refused the destroy, which is a per-species prefab fact and needs the depending component stripped first. Either way the spawn will fire VANILLA quest events on death.")); } if (componentsInChildren3.Length != 0) { Plugin.Log.LogMessage((object)$"[SPAWN] stripped {componentsInChildren3.Length - num5} QuestEventOnDeath from '{((Object)go).name}' (StripQuestEvents)."); } int num6 = 0; AchievementOnCharacterDeath[] componentsInChildren5 = go.GetComponentsInChildren(true); foreach (AchievementOnCharacterDeath val8 in componentsInChildren5) { if ((Object)(object)val8 != (Object)null) { Object.DestroyImmediate((Object)(object)val8); num6++; } } AchievementSetStatOnCharacterDeath[] componentsInChildren6 = go.GetComponentsInChildren(true); foreach (AchievementSetStatOnCharacterDeath val9 in componentsInChildren6) { if ((Object)(object)val9 != (Object)null) { Object.DestroyImmediate((Object)(object)val9); num6++; } } if (num6 > 0) { Plugin.Log.LogMessage((object)$"[SPAWN] stripped {num6} Achievement*OnCharacterDeath from '{((Object)go).name}' (StripQuestEvents)."); } } if (Plugin.ForceLootableEnabled.Value) { int num7 = 0; LootableOnDeath[] componentsInChildren7 = go.GetComponentsInChildren(true); foreach (LootableOnDeath val10 in componentsInChildren7) { if ((Object)(object)val10 != (Object)null && !((Behaviour)val10).enabled) { ((Behaviour)val10).enabled = true; num7++; } } if (num7 > 0) { Plugin.Log.LogMessage((object)$"[SPAWN] re-enabled {num7} disabled LootableOnDeath on '{((Object)go).name}' (ForceLootableEnabled — corpse-loot backstop)."); } } if (opts.Faction.HasValue) { ch.Faction = opts.Faction.Value; } if (!identity.HasValue && opts.LifetimeSeconds.HasValue) { ch.Lifetime = opts.LifetimeSeconds.Value; } GhostVisuals.PreActivationFix(go, ch); GhostVisuals.RootScaleFix(go, ch); } internal static void DeferAiCulling(GameObject go, string why) { if ((Object)(object)go == (Object)null) { return; } try { CharAIDisable component = go.GetComponent(); if ((Object)(object)component == (Object)null) { ModLog log = Plugin.Log; if (log != null) { log.LogMessage((object)("[SPAWN] no CharAIDisable on " + why + " — nothing to defer. Two benign-to-serious causes: the donor's serialized InstantiateCharAIDisable is false (healthy — this species just never distance-culls), or the template truly has no AI graph (which TemplateAiGate refuses upstream, so a statue reaching here would be a gate bug).")); } } else { component.AiLastCloseCheck = Time.time + 1f; } } catch (Exception ex) { ModLog log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("[SPAWN] AI-cull grace stamp threw for " + why + " (" + ex.GetType().Name + ": " + ex.Message + ") — a far mint may be culled before Character.Start runs (BUG-FARCACHERESTORE).")); } } } } [BepInPlugin("cobalt.spawnkit", "SpawnKit", "0.5.2")] [BepInDependency("cobalt.forgekit", "0.4.10")] [BepInDependency("cobalt.donorkit", "0.1.7")] [BepInDependency("cobalt.companionkit", "0.4.20")] [BepInDependency("cobalt.netkit", "0.2.6")] public class Plugin : BaseUnityPlugin { public const string GUID = "cobalt.spawnkit"; public const string NAME = "SpawnKit"; public const string VERSION = "0.5.2"; public const string COMPAT_SINCE = "0.5.0"; internal static ModLog Log; internal static Plugin Instance; public static ConfigEntry Enabled; public static ConfigEntry AllowSpawnInRoom; public static ConfigEntry MaxActiveSpawns; public static ConfigEntry SpawnDistance; public static ConfigEntry MaxCachedTemplates; public static ConfigEntry DefaultCorpsePolicy; public static ConfigEntry DefaultCorpseLingerSeconds; public static ConfigEntry ForceLootableEnabled; public static ConfigEntry EnableMenu; public static ConfigEntry MenuKey; public static ConfigEntry EnableExpeditions; public static ConfigEntry ConfirmMenuExpedition; public static ConfigEntry EnableCoopSpawns; public static ConfigEntry DespawnOnMirrorFailure; public static ConfigEntry MirrorQueueTimeoutSeconds; public static ConfigEntry GateColdMirrorHarvest; public static ConfigEntry MirrorAcquireTimeoutSeconds; public static ConfigEntry MirrorHarvestBudget; public static ConfigEntry MirrorDonorChainCap; public static ConfigEntry GuestPrewarm; public static ConfigEntry GuestPrewarmBudget; public static ConfigEntry GuestDonorCycleCeiling; public static ConfigEntry GuestEquipWeapon; public static ConfigEntry ShipWeaponIdentity; public static ConfigEntry PostActivationVisualPass; public static ConfigEntry GhostDiagnostics; public static ConfigEntry RigReinitPass; public static ConfigEntry GhostVisualsFix; public static ConfigEntry ClearAiQuestGate; public static ConfigEntry CorpseViewRelease; public static ConfigEntry AiDisableDistance; public static ConfigEntry RoomWarmMode; public static ConfigEntry WarmSetPublishSeconds; public static ConfigEntry RoomWarmRequestCap; public static ConfigEntry RoomWarmRequestTimeoutSeconds; public static ConfigEntry RoomWarmRequestMaxTimeoutSeconds; public static ConfigEntry RoomWarmRequestAttempts; public static ConfigEntry VerboseNet; public static ConfigEntry HeartbeatSeconds; private CommandRegistry _commands; private CommandChannel _channel; private float _nextWatch; private static Character LocalPlayer => Lifecycle.FirstLocalCharacterOrNull(); [MethodImpl(MethodImplOptions.NoInlining)] private static void DeclareKitContracts() { KitContract.Declare("SpawnKit", "cobalt.forgekit", "0.4.10"); KitContract.Declare("SpawnKit", "cobalt.donorkit", "0.1.7"); KitContract.Declare("SpawnKit", "cobalt.companionkit", "0.4.20"); KitContract.Declare("SpawnKit", "cobalt.netkit", "0.2.6"); } private void TryDeclareKitContracts() { try { DeclareKitContracts(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[CONTRACT] kit handshake unavailable (" + ex.GetType().Name + ") — is ForgeKit older than this mod?")); } } internal void Awake() { //IL_0253: Unknown result type (might be due to invalid IL or missing references) //IL_05b8: Unknown result type (might be due to invalid IL or missing references) //IL_05c2: Expected O, but got Unknown //IL_05e5: Unknown result type (might be due to invalid IL or missing references) //IL_05ea: Unknown result type (might be due to invalid IL or missing references) //IL_05f5: Unknown result type (might be due to invalid IL or missing references) //IL_0600: Unknown result type (might be due to invalid IL or missing references) //IL_060b: Unknown result type (might be due to invalid IL or missing references) //IL_0622: Expected O, but got Unknown //IL_061d: Unknown result type (might be due to invalid IL or missing references) //IL_0627: Expected O, but got Unknown //IL_062c: Unknown result type (might be due to invalid IL or missing references) //IL_0632: Expected O, but got Unknown //IL_06f8: Unknown result type (might be due to invalid IL or missing references) TryDeclareKitContracts(); Log = ModLog.Bind((BaseUnityPlugin)(object)this, ((BaseUnityPlugin)this).Logger); Instance = this; TemplateStore.SpawnableNormalizer = SpawnTemplates.NormalizeRegistered; Enabled = ((BaseUnityPlugin)this).Config.Bind("Spawner", "Enabled", true, "Kill-switch: false refuses new spawns (API + verbs). despawnall/spawndump keep working."); AllowSpawnInRoom = ((BaseUnityPlugin)this).Config.Bind("Spawner", "AllowSpawnInRoom", false, "Phase 3 changed this from 'the only in-room path' to THE UNSAFE OVERRIDE: with [Coop] EnableCoopSpawns=true, in-room spawns work whenever every peer handshakes a compatible SpawnKit — this flag is only consulted when they DON'T (unmodded/incompatible peer, or coop disabled). true = spawn anyway: modded peers still mirror, but the un-handshaken ones get the pre-Phase-3 ghost (Unity warn spam for the unknown viewID + the master locked in unwinnable ghost-combat against their invulnerable replicas — review 2.5a). Solo/offline play never consults this gate."); MaxActiveSpawns = ((BaseUnityPlugin)this).Config.Bind("Spawner", "MaxActiveSpawns", 12, "Refuse spawns that would push the live count (incl. pending) past this cap. Global across ALL consumers. (Default raised 8 -> 12 in the 2026-08-09 perf wave, alongside DangerousRoads' far-despawn layer; NB BepInEx never migrates a changed default into an existing cfg.)"); CorpseViewRelease = ((BaseUnityPlugin)this).Config.Bind("Spawner", "CorpseViewRelease", false, "OPT-IN (perf wave 2026-08-09): free a dead spawn's PhotonView id at death instead of parking it until scene unload. Under the default Vanilla corpse policy each corpse otherwise reserves its viewID for the rest of the zone visit, and Photon's allocation pool hard-fails at 999 cumulative ids per session — long one-zone sessions with many kills eat toward that. Mechanism: the corpse's view is NEUTRALIZED (deregistered + id-zeroed + send-blocked; the corpse itself + loot are untouched) and the id released immediately. false = the shipped mute-and-park behavior."); AiDisableDistance = ((BaseUnityPlugin)this).Config.Bind("Perf", "AiDisableDistance", 0f, "0 = off (donor behavior). >0 = stamp the VANILLA per-character AI sleep radius (CharacterAI.DistanceToUpdate, metres — horizontal AND vertical) on every spawn at mint: the engine's own CharAIDisable then suspends the creature's AI/bars while every player is farther than this, exactly like scene creatures whose designers set it. Cheap headroom for high MaxActiveSpawns; applies to NEW spawns only. Suggested ~60-80 if used."); SpawnDistance = ((BaseUnityPlugin)this).Config.Bind("Spawner", "SpawnDistance", 5f, "Preferred ground distance from the player for the placement ring probe (meters). Per-spawn override: SpawnOptions.Distance."); MaxCachedTemplates = ((BaseUnityPlugin)this).Config.Bind("Spawner", "MaxCachedTemplates", 12, "Evict the least-recently-used species template past this count (its next spawn re-harvests the donor scene). Each cached template is a full creature clone pinning its meshes/textures/audio, so an unbounded cache grows memory for the whole session. 0 = unlimited (warn-nudge only)."); DefaultCorpsePolicy = ((BaseUnityPlugin)this).Config.Bind("Spawner", "DefaultCorpsePolicy", (CorpsePolicy)0, "Corpse fate after a real death when SpawnOptions.Corpse is null. Vanilla = the engine's own semantic (no corpse GC exists — corpse + loot persist until scene unload). NoBody = destroy the corpse (AND its loot bag with it) DefaultCorpseLingerSeconds after the death resolves. Stamped per-spawn at mint — an edit applies to NEW spawns only, never already-live ones."); DefaultCorpseLingerSeconds = ((BaseUnityPlugin)this).Config.Bind("Spawner", "DefaultCorpseLingerSeconds", 0f, "Seconds a NoBody corpse lingers after death before it is destroyed (death-anim time). 0 = the tick after the death resolves; negative clamps to 0. Per-spawn override: SpawnOptions.CorpseLingerSeconds / spawnex linger=N."); ForceLootableEnabled = ((BaseUnityPlugin)this).Config.Bind("Spawner", "ForceLootableEnabled", true, "Corpse-loot backstop (V-TLOOT): re-enable any DISABLED LootableOnDeath on a spawn at mint so its corpse is lootable. A disabled component makes OnDeath early-return (no prompt, no error). Quest-safe — LootableOnDeath is self-contained; nothing else in the game reads it, and QuestEventOnDeath (the quest-strip target) is a separate receiver. Does NOT invent loot: a creature with no drops configured (a scripted/boss reward) still shows no prompt — see the [LOOT] verdict line at each spawn death."); PostActivationVisualPass = ((BaseUnityPlugin)this).Config.Bind("Visual", "PostActivationPass", true, "MP §2/§2b fix, kill-switch (live via reloadcfg / the skvisual verb): after each mint's two-frame dwell, measure the spawn (renderers, render-ready, BakeMesh bounds, stacked bones, animator) and — only when the unit-tested gate says so — rebuild missing Start-built visuals (ghosts spawn INVISIBLE off the frozen-donor template otherwise) and/or Animator.Rebind()+Update(0) (the Medyse vertical-line class). Healthy spawns are measured, logged ([SPAWN] visual), and untouched. false = 2026-08-02 behavior."); GhostDiagnostics = ((BaseUnityPlugin)this).Config.Bind("Visual", "GhostDiag", true, "BUG-GHOSTINERTSPAWN diagnostics: emit the [GHOSTDIAG] census (visuals-holder init state, hitbox counts + their layers, ragdoll hitbox colliders, attack transforms + weapon linecast sockets, live bone spread) at each mint before and after the visual pass, plus one delayed reading once the level loader is done. It is the block that tells an ACTIVATION-ORDERING failure (hitboxes collected while the visuals subtree was switched off, so the sweep has nothing to hit) apart from a POSE failure (the rig never leaves its collapsed vertical-line rest pose, so every collider sits in a 4cm column). Verbose by design and cheap; turn it off once the bug is closed."); RigReinitPass = ((BaseUnityPlugin)this).Config.Bind("Visual", "RigReinitPass", true, "BUG-GHOSTINERTSPAWN fix, kill-switch (live via the ghostfix verb): after the visual pass has activated the visuals subtree, re-run the vanilla init chain Character.Start already spent — InitDefaultVisuals, InitHitboxes, re-latch m_hitboxes/RagdollRoot, InitRagdoll, SetRagdollActive(false) — so the hurtboxes actually exist and land on the 'Hitbox' layer the weapon sweep masks, and force one real Animator evaluation so the rig leaves its rest pose. Self-measuring: a healthy spawn reports steps=none and is untouched. false = 2026-08-07 behavior (Ghost spawns invisible, unhittable and harmless)."); GhostVisualsFix = ((BaseUnityPlugin)this).Config.Bind("Visual", "GhostVisualsFix", true, "BUG-GHOSTINERTSPAWN spike-5 fix-at-cause, kill-switch (live via reloadcfg): for dynamic-visuals clones (UseLegacyVisual=false — the Ghost/spirit family), (F1) force the CharacterVisuals subtree + ragdoll root ACTIVE while the per-spawn clone is still inactive, so Character.Start's one-shot active-only init chain (InitVisuals/InitHitboxes/InitRagdoll) sees the whole rig; and (F2) a uid-scoped ('SK_') Harmony postfix on Character.InitVisuals runs InitDefaultVisuals for our clones when both vanilla routes skip it (the holder-already-exists clone branch and the IsGameplayLoading&&IsAI gate). Baked-mesh beasts are untouched. With this on, the [Visual] RigReinitPass repair should measure steps=none on ghost spawns. false = 2026-08-08 behavior."); ClearAiQuestGate = ((BaseUnityPlugin)this).Config.Bind("Spawner", "ClearAiQuestGate", true, "Spike-7 (dormant-AI boss class, Calixa prime suspect): some donors gate their AI root on a quest event (CharacterAI.m_aiActiveOnQuestEvent) — cloned into a spawn, the OnEnable check reads the event as un-fired and the AI root deactivates, leaving an invisible motionless statue. true = clear the gate reference at TEMPLATE build (empty EventUID short-circuits the gate to 'active'); the [TEMPLATE] census line records the pre-clear truth and the clear logs the old value. false = raw-clone template (pre-2026-08-08 behavior)."); EnableMenu = ((BaseUnityPlugin)this).Config.Bind("Menu", "EnableMenu", true, "The in-game spawn menu (the MenuKey toggle + 'spawnmenu' verb). On by default — it is the mod's primary interface; the *_cmd.txt channel is the fallback. false = the menu never opens and its CharacterUI.IsMenuFocused Harmony patch is never applied."); MenuKey = ((BaseUnityPlugin)this).Config.Bind("Menu", "MenuKey", new KeyboardShortcut((KeyCode)92, Array.Empty()), "Toggles the in-game spawn menu (needs EnableMenu=true). F6-F12 are taken by other kit mods (F11 was the Bug-26 three-mod collision); Backslash is the classic debug-menu rebind."); Keybinds.Claim("SpawnKit", "toggle the spawn menu", MenuKey); EnableExpeditions = ((BaseUnityPlugin)this).Config.Bind("Expedition", "EnableExpeditions", true, "Let SpawnKit FETCH a region-only species (one whose every donor is an oversized region scene the additive harvest refuses) by running a real expedition: the whole party rides two vanilla loading screens to the donor region and back, and the game SAVES on each leg. One trip caches every species that region donates. false = those rows behave as before (they can only spawn if something else — Beastwhispering's 'expedition' verb or its boot auto-warm — already cached the body). Host/offline only."); ConfirmMenuExpedition = ((BaseUnityPlugin)this).Config.Bind("Expedition", "ConfirmMenuExpedition", true, "In the spawn menu, require a SECOND click to launch an expedition (the button flips to 'Confirm?' for 5s). A single stray click on a 166-row scrolling list should not teleport the party and write a save. false = one click goes immediately."); EnableCoopSpawns = ((BaseUnityPlugin)this).Config.Bind("Coop", "EnableCoopSpawns", true, "Phase-3 co-op spawn replication: a host spawn in a Photon room is broadcast (sk.spawn over the SK_Bus relay) to every guest running a compatible SpawnKit, which mirrors it as a same-viewID vanilla-shaped replica the master streams. In-room spawns are HANDSHAKE-GATED: every peer must have sent sk.hello, else the spawn refuses (PeersNotReady) — AllowSpawnInRoom=true overrides. false = the exact pre-Phase-3 behavior (refuse in-room; AllowSpawnInRoom=true = the old ghost). Live via reloadcfg — but NB flipping OFF with live mirrors stops sk.gone: guests keep their replicas until the next scene change (README trap row)."); DespawnOnMirrorFailure = ((BaseUnityPlugin)this).Config.Bind("Coop", "DespawnOnMirrorFailure", true, "When a guest reports sk.fail (it could not mirror a spawn: no donor/template on its machine, timeout, mint failure), silently despawn that spawn everywhere. A spawn one guest can't see is the old ghost-combat hazard — despawning is the safe default. false = keep it (that guest gets the pre-Phase-3 ghost behavior)."); MirrorQueueTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind("Coop", "MirrorQueueTimeoutSeconds", 20f, "How long a guest keeps a received sk.spawn queued (waiting for player-ready / a matching scene) before dropping it with sk.fail(timeout). The whole wait, the master is streaming at a viewID this machine doesn't have yet — PUN logs a warning AND runs a FindObjectsOfType scene scan ~10×/s per unknown view (NetworkingPeer.cs:3404+2827) — so this bounds that cost. Keep short. NB the clock only starts once this guest is in-world (player-ready + loading done) — a guest still on a loading screen reports sk.fail(not-ready) instead, which the master does NOT despawn on."); GateColdMirrorHarvest = ((BaseUnityPlugin)this).Config.Bind("Coop", "GateColdMirrorHarvest", true, "GUEST-COLD-HARVEST (the 2026-08-20 tester disconnect). A guest that receives sk.spawn for a species it holds NO template for has to additively load a whole donor scene to mint it — measured 3.6s, 12.1s and 14.7s in the field, in combat, in a dungeon (a WARM mint is 60-130ms). With this on, such a spawn is refused with sk.fail(cold-unsafe) whenever it is a bad moment (in combat, loader/save busy, or MirrorHarvestBudget spent), and the species is queued to be warmed as soon as it IS a good moment — so the next one mints from cache. false = the old behaviour: harvest immediately, whatever else is going on."); MirrorAcquireTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind("Coop", "MirrorAcquireTimeoutSeconds", 25f, "Hard ceiling on the template-ACQUIRE leg of a guest mirror (donor-scene chain walk). MirrorQueueTimeoutSeconds only bounds the time an sk.spawn sits QUEUED — once the mirror routine starts, nothing re-checked the clock, which is how a 12.1s and a 14.7s acquire got through. On expiry: abandon + sk.fail(timeout). 0 disables the ceiling."); MirrorHarvestBudget = ((BaseUnityPlugin)this).Config.Bind("Coop", "MirrorHarvestBudget", 4, "How many COLD guest-mirror DONOR CYCLES one session may spend before refusing the rest with sk.fail(cold-unsafe). Donor cycles are a finite resource — the LightProbesManager crash sits around 11-17 of them (DonorHarvest.cs) — and these are spent on the HOST's spawn choices, not this player's. Counted as real DonorHarvest.CyclesThisSession deltas, so an acquire that rides another routine's in-flight harvest or adopts an expedition body is free. Resets when the room changes. 0 = unlimited (pre-2026-08-20 behaviour)."); GuestPrewarm = ((BaseUnityPlugin)this).Config.Bind("Coop", "GuestPrewarm", true, "GUEST VOLUNTARY WARM (2026-08-24). This machine, as a GUEST, may warm a species on its own initiative — the menu's Prewarm button and the spawnprewarm verb — instead of being refused as host-only. The ask goes through the SAME deferral rules as a host's sk.want (never in combat, never while the loader/save is busy) and is charged to its own bucket (GuestPrewarmBudget). false = today's behaviour: Prewarm greyed with '(host only)', spawnprewarm refused. Spawn and Warm (trip) stay host-only regardless — those are authority, not warmth."); GuestPrewarmBudget = ((BaseUnityPlugin)this).Config.Bind("Coop", "GuestPrewarmBudget", 4, "Donor cycles a guest may spend per room on its OWN prewarm asks — a bucket separate from MirrorHarvestBudget, so the host's spawn choices and the player's own never compete. Resets when the room changes. 0 = unlimited (the session ceiling still applies)."); GuestDonorCycleCeiling = ((BaseUnityPlugin)this).Config.Bind("Coop", "GuestDonorCycleCeiling", 10, "Hard per-SESSION cap on donor cycles a guest spends across BOTH buckets (host wants + voluntary). Donor cycles are the LightProbesManager crash resource (~11-17 per process, DonorHarvest.cs); per-room buckets reset, this one does not — it reads DonorKit's real session counter, so cycles spent before joining the room count too. A reached ceiling refuses every guest warm with 'session-ceiling' and publishes budget 0 so the host stops asking. 0 = no ceiling."); MirrorDonorChainCap = ((BaseUnityPlugin)this).Config.Bind("Coop", "MirrorDonorChainCap", 2, "Max donor scenes an in-session COLD guest mirror may try before giving up. DonorHarvest walks a species' whole candidate list (the field case walked 'Ghost' across an 11-scene chain: one scene FAILED after 7.18s, the next took 4.85s, 12.1s total). The host-side harvest keeps the full walk, and so does the out-of-combat WARM path — neither is hitching a live fight, and a capped walk there would abandon a species that is harvestable further down the list. 0 = no cap."); RoomWarmMode = ((BaseUnityPlugin)this).Config.Bind("Coop", "RoomWarmMode", (RoomWarmMode)1, "WARM MIRROR (2026-08-20) — how strictly a spawn is gated on whether the WHOLE PARTY can already mint the species. Every peer publishes its own warm set (sk.warmset), so the host can know, before it picks, who would have to additively load a donor scene to mirror the spawn (measured 3.6-14.7s, in combat, in the field). Local = ignore peers entirely (the pre-co-op gate; solo play is byte-identical in every mode). RoomDegraded = spawn anyway, but NAME the cold peers in the log — the default, because a road ambush that silently stops arming is a worse experience than one guest paying a load. RoomStrict = refuse the spawn; pick this when the party would rather see fewer enemies than have anyone stall mid-fight. ENFORCED as of Phase B (2026-08-20): RoomStrict REFUSES the spawn (FailReason.SpeciesColdOnPeer, the log names the actor ids); RoomDegraded spawns it AND asks the cold guests to warm the species (sk.want) so the next one is instant for them; Local never reads a peer at all. Solo and rooms where nobody has published a warm set are byte-identical in every mode. Dev verbs escape the gate per call with a 'force' token (spawn/spawnex) — deliberately not a config, because a forgotten global would silently disable the gate for every consumer."); WarmSetPublishSeconds = ((BaseUnityPlugin)this).Config.Bind("Coop", "WarmSetPublishSeconds", 1f, "WARM MIRROR: minimum seconds between two publishes of THIS machine's warm set. Every event that could move the set (a template cached or evicted, a finished warm, a proven dead end, scene-ready) is coalesced into at most one publish per this interval. Raising it costs the room freshness, not correctness; lowering it below ~0.5s buys little, because the store change-latches on the encoded BYTES — an unchanged set sends nothing however often it is offered, and it still heartbeats every 30s so a dropped message cannot leave a mirror stale forever."); RoomWarmRequestCap = ((BaseUnityPlugin)this).Config.Bind("Coop", "RoomWarmRequestCap", 3, "WARM MIRROR (Phase B): max species asked of ONE peer per Spawner.RequestRoomWarm call. A consumer that hands over its whole roster (DangerousRoads' region tables run to dozens of species) would otherwise exhaust a guest's session harvest allowance instantly — MirrorHarvestBudget ships at 4 — and thrash its 12-entry warm LRU, leaving it cold on everything, which is the exact state this mechanism exists to prevent. Three is a fight's worth of variety without spending the budget in one breath; the caller's priority ORDER decides which three, and the same order goes to every peer so the room-wide intersection grows instead of two guests warming disjoint halves. 0 or negative = uncapped (you have taken responsibility for the roster size)."); RoomWarmRequestTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind("Coop", "RoomWarmRequestTimeoutSeconds", 30f, "WARM MIRROR (Phase C): seconds the host waits for a guest to answer an sk.want before it asks again. This IS the repeat rule — the same (peer, species) is never re-sent inside this window — and it is deliberately an order of magnitude longer than the 1s timeouts the Unity Netcode dynamic-prefab prior art uses. Answering a want means running an additive donor-scene load (measured 3.6s, 12.1s and 14.7s in the field), and the guest deliberately DEFERS it to a safe moment — out of combat, no save pending. A short timeout would re-ask a guest that is behaving perfectly and then abandon it while its first harvest is still running. There is no answer message to wait for: the confirmation is the guest's own warm-set row changing (sk.warmset), which retires the ask the instant it lands."); RoomWarmRequestMaxTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind("Coop", "RoomWarmRequestMaxTimeoutSeconds", 120f, "WARM MIRROR (Phase C): ceiling on the doubling backoff between sk.want retries. Each retry waits twice as long as the last (30s, 60s, 120s at the shipped defaults) because a guest that missed the first ask is usually BUSY, not broken, and asking harder makes it worse. The cap keeps a long session from growing an unbounded silent gap. Values below RoomWarmRequestTimeoutSeconds are clamped up to it (no backoff at all), never rejected — a nonsense value must degrade, not throw inside a game tick."); RoomWarmRequestAttempts = ((BaseUnityPlugin)this).Config.Bind("Coop", "RoomWarmRequestAttempts", 3, "WARM MIRROR (Phase C): how many times the host sends the SAME sk.want before it stops asking for that (peer, species) for the rest of the session, logging exactly one [MIRROR] give-up line. Three spread over the doubling backoff is ~3.5 real minutes at the shipped defaults — long enough to cover a guest fighting through an ambush before it can safely harvest, short enough that a guest which simply cannot comply stops costing wire traffic forever. A peer that proves the species a dead end, or that runs out of donor-harvest budget, is given up on IMMEDIATELY instead — that is a structural no, not a slow yes. Minimum 1."); GuestEquipWeapon = ((BaseUnityPlugin)this).Config.Bind("Coop", "GuestEquipWeapon", true, "MP §1 guest-damage fix, kill-switch (live via reloadcfg / the skweapon verb): on a guest, bind each replica's hand-slot weapon (HumanoidWeapon.EnsureEquipped from the census/watch). Vanilla player damage is local-only — an enemy can hurt a guest ONLY via the guest's own replica running its own hit detection, and that needs CurrentWeapon bound. false = the 2026-08-02 broken behavior (replicas swing and never hit guests)."); ShipWeaponIdentity = ((BaseUnityPlugin)this).Config.Bind("Coop", "ShipWeaponIdentity", true, "MP §1 guest-damage fix, kill-switch (master-side): ship the spawn's RightHand/LeftHand item ItemID+UID on the spawn payload (fields 13-16) so the guest mints the SAME weapon identity and vanilla's UID-keyed item-sync equips + sustains it (no long-sync destroy, no phantom item copy). false = guests mint fresh random item UIDs (GuestEquipWeapon's bind then decays when the long-sync reaper destroys the unmatched weapon; the watch re-binds each tick)."); VerboseNet = ((BaseUnityPlugin)this).Config.Bind("Coop", "VerboseNet", true, "DEPRECATED — use [Diag] LogLevel (the guest-side [MIRROR] mirror-queue detail lines ride the Verbose tier). Setting this false still suppresses them regardless of LogLevel. The per-message [SKNET] send/receive lines were already NetKit's — see [Diag] LogLevel in the NetKit .cfg."); HeartbeatSeconds = ((BaseUnityPlugin)this).Config.Bind("Coop", "HeartbeatSeconds", 30f, "DEPRECATED / IGNORED since the NetKit migration — the co-op heartbeat interval is now [Net] HeartbeatSeconds in the NetKit .cfg (it drives the [SKNET] hb line for every channel). This key is kept only for backward compatibility and has no effect."); if (HeartbeatSeconds.Value != (float)((ConfigEntryBase)HeartbeatSeconds).DefaultValue) { Log.LogWarning((object)($"[SKNET] [Coop] HeartbeatSeconds={HeartbeatSeconds.Value} is DEPRECATED and IGNORED " + "— the heartbeat interval now rides [Net] HeartbeatSeconds in NetKit's cfg (cfgs never migrate descriptions; edit NetKit's cfg instead).")); } RagdollProbe.SpawnedUidClassifier = SpawnUid.IsSpawnUid; _commands = new CommandRegistry(ModLog.op_Implicit(Log)); RegisterVerbs(); _channel = new CommandChannel("SpawnKit_cmd.txt", ModLog.op_Implicit(Log), _commands, 0.5f, true, true, new CatalogInfo { ModGuid = "cobalt.spawnkit", ModName = "SpawnKit", ModVersion = "0.5.2", ConfigSource = () => ((BaseUnityPlugin)this).Config }); Harmony val = new Harmony("cobalt.spawnkit"); val.PatchAll(typeof(GhostVisuals.InitVisualsPatch)); val.PatchAll(typeof(GhostVisuals.VisiblePatch)); SpawnNet.Init(); TemplateStore.Spawnables.Changed += delegate(string k) { WarmMirror.MarkDirty("template " + k); }; TemplateStore.CompanionBodies.Changed += delegate(string k) { WarmMirror.MarkDirty("body template " + k); }; if (EnableMenu.Value) { val.PatchAll(typeof(CursorControl)); ((Component)this).gameObject.AddComponent(); } Log.LogMessage((object)("SpawnKit 0.5.2 loaded — 'help' in BepInEx/config/SpawnKit_cmd.txt lists the verbs" + (EnableMenu.Value ? $"; spawn menu on {MenuKey.Value}." : "."))); Log.LogMessage((object)("[SPAWNKIT] build " + BuildStamp.Read(((object)this).GetType().Assembly) + " @ " + ((object)this).GetType().Assembly.Location)); } internal void Update() { _channel.Tick(); GuestReplicas.Tick(); WarmMirror.Tick(); SpawnNet.TickForgiven(); if (Time.unscaledTime >= _nextWatch) { _nextWatch = Time.unscaledTime + 0.5f; EnemySpawner.WatchTick(); } } private void RegisterVerbs() { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Expected O, but got Unknown //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Expected O, but got Unknown //IL_0198: Unknown result type (might be due to invalid IL or missing references) //IL_019e: Expected O, but got Unknown //IL_01eb: Unknown result type (might be due to invalid IL or missing references) //IL_01f1: Expected O, but got Unknown //IL_0200: Unknown result type (might be due to invalid IL or missing references) //IL_0206: Expected O, but got Unknown //IL_06d6: Unknown result type (might be due to invalid IL or missing references) //IL_06db: Unknown result type (might be due to invalid IL or missing references) //IL_06f2: Expected O, but got Unknown VerbHost val = new VerbHost(_commands, ModLog.op_Implicit(Log), (Func)(() => LocalPlayer)); string[] array; try { array = new List(Spawner.SpeciesKeys()).ToArray(); } catch { array = null; } val.Register("spawn", "spawn [count] [force] — spawn as live hostile(s) beside the player (thin wrapper over Spawner.Spawn). 'force' ignores the room-wide warmth gate for this call (SpawnOptions.IgnoreRoomWarm): the cold guest pays the donor load, which is often exactly what you want to watch.", (Action)delegate(VerbContext ctx) { string[] array2 = ctx.Parts; bool flag = array2.Length > 1 && string.Equals(array2[^1], "force", StringComparison.OrdinalIgnoreCase); if (flag) { string[] array3 = new string[array2.Length - 1]; Array.Copy(array2, array3, array2.Length - 1); array2 = array3; } ParseSpeciesAndCount(array2, out var species, out var count); if (string.IsNullOrEmpty(species)) { Log.LogWarning((object)"[SPAWN] usage: spawn [count] [force]"); } else { for (int i = 0; i < count; i++) { Spawner.Spawn(species, new SpawnOptions { OwnerTag = "spawnkit.verb", IgnoreRoomWarm = flag }, delegate(SpawnHandle h) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) if (h.State != SpawnState.Alive) { Log.LogWarning((object)$"[SPAWN] verb spawn '{h.SpeciesKey}' -> {h.State} ({h.FailReason})."); } }); } } }, "[CMD]", false, true, true, (string)null, (ArgSpec[])(object)new ArgSpec[2] { new ArgSpec("species", "species", false, array, (string)null), new ArgSpec("count", "int", true, (string[])null, "1") }); val.Register("despawnall", "despawnall [kill] — remove every tracked spawn (all owners); 'kill' routes through the real damage pipeline (death anim + loot).", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; bool kill = parts.Length > 1 && string.Equals(parts[1], "kill", StringComparison.OrdinalIgnoreCase); Spawner.DespawnAll(null, kill); }, "[CMD]", false, true, true, (string)null, (ArgSpec[])null); val.Register("spawndump", "Per-spawn live state: uid/alive/hp/dist/AI state/target — plus the template cache.", (Action)delegate { Log.LogMessage((object)EnemySpawner.Dump()); Log.LogMessage((object)SpawnTemplates.Dump()); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("spawnclearcache", "Destroy all cached species templates (next spawn re-harvests).", (Action)delegate { int num = SpawnTemplates.Clear(); Log.LogMessage((object)$"[TEMPLATE] cleared {num} cached template(s)."); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("spawnprep", "spawnprep — ONE donor load builds templates for every table species listing that scene (light-probe cycle budget amortizer).", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; string text = string.Join(" ", parts, 1, Math.Max(0, parts.Length - 1)).Trim(); if (text.Length == 0) { Log.LogWarning((object)"[PREP] usage: spawnprep "); } else { ((MonoBehaviour)this).StartCoroutine(SpawnTemplates.AcquireScene(text, delegate { })); } }, "[CMD]", false, true, true, (string)null, (ArgSpec[])(object)new ArgSpec[1] { new ArgSpec("scene", "scene", false, (string[])null, (string)null) }); val.Register("spawnex", "spawnex [dist=N] [life=N] [faction=Name] [owner=tag] [body=vanilla|none] [linger=N] [keepquest] [force] — one spawn with per-spawn SpawnOptions (the facade's full surface, verb-shaped).", (Action)delegate(VerbContext ctx) { //IL_00e9: Unknown result type (might be due to invalid IL or missing references) string[] parts = ctx.Parts; SpawnVerbArgs val2 = SpawnVerbArgs.Parse(parts); foreach (string unknownOption in val2.UnknownOptions) { Log.LogWarning((object)("[SPAWN] spawnex: unrecognized option '" + unknownOption + "' (knowns: dist= life= faction= owner= body= linger= keepquest force).")); } if (val2.Species.Length == 0) { Log.LogWarning((object)"[SPAWN] usage: spawnex [dist=N] [life=N] [faction=Name] [owner=tag] [body=vanilla|none] [linger=N] [keepquest] [force]"); } else { SpawnOptions spawnOptions = new SpawnOptions { Distance = val2.Distance, LifetimeSeconds = val2.LifetimeSeconds, CorpseLingerSeconds = val2.CorpseLingerSeconds, OwnerTag = (val2.OwnerTag ?? "spawnkit.verb"), StripQuestEvents = !val2.KeepQuestEvents, IgnoreRoomWarm = val2.IgnoreRoomWarm }; if (val2.Faction != null) { if (!Enum.TryParse(val2.Faction, ignoreCase: true, out Factions result)) { Log.LogWarning((object)("[SPAWN] spawnex: unknown faction '" + val2.Faction + "' — valid: " + string.Join(", ", Enum.GetNames(typeof(Factions))) + ".")); return; } spawnOptions.Faction = result; } if (val2.Body != null) { if (string.Equals(val2.Body, "vanilla", StringComparison.OrdinalIgnoreCase)) { spawnOptions.Corpse = (CorpsePolicy)0; } else { if (!string.Equals(val2.Body, "none", StringComparison.OrdinalIgnoreCase)) { Log.LogWarning((object)("[SPAWN] spawnex: unknown body policy '" + val2.Body + "' — valid: vanilla, none.")); return; } spawnOptions.Corpse = (CorpsePolicy)1; } } Spawner.Spawn(val2.Species, spawnOptions, delegate(SpawnHandle h) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) if (h.State != SpawnState.Alive) { Log.LogWarning((object)$"[SPAWN] spawnex '{h.SpeciesKey}' -> {h.State} ({h.FailReason})."); } }); } }, "[CMD]", false, true, true, (string)null, (ArgSpec[])(object)new ArgSpec[2] { new ArgSpec("species", "species", false, array, (string)null), new ArgSpec("options", "rest", true, (string[])null, (string)null) }); val.Register("despawnowner", "despawnowner [kill] — remove only the spawns tagged with (Spawner.DespawnAll scoped; V40).", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; bool flag = parts.Length > 2 && string.Equals(parts[^1], "kill", StringComparison.OrdinalIgnoreCase); int num = (flag ? (parts.Length - 1) : parts.Length); string text = string.Join(" ", parts, 1, Math.Max(0, num - 1)).Trim(); if (text.Length == 0) { Log.LogWarning((object)"[SPAWN] usage: despawnowner [kill]"); } else { Spawner.DespawnAll(text, flag); } }, "[CMD]", false, true, true, (string)null, (ArgSpec[])null); val.Register("spawnprewarm", "spawnprewarm — pay the donor harvest now so the first spawn is instant (Spawner.Prewarm). On a GUEST ([Coop] GuestPrewarm) it is queued as a VOLUNTARY warm and runs at the next safe moment.", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; string species = string.Join(" ", parts, 1, Math.Max(0, parts.Length - 1)).Trim(); if (species.Length == 0) { Log.LogWarning((object)"[TEMPLATE] usage: spawnprewarm "); } else { Spawner.Prewarm(species, delegate(bool ok) { Log.LogMessage((object)("[TEMPLATE] prewarm '" + species + "': " + (ok ? "resident" : "FAILED (see log)") + ".")); }); } }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("expeditionreset", "Force a stuck expedition guard open — both CompanionKit's and SpawnKit's (the fix for 'the spawn menu stopped opening'). Does NOT teleport anyone.", (Action)delegate { Log.LogMessage((object)ExpeditionHarvest.ForceReset()); Log.LogMessage((object)ExpeditionRun.ForceReset()); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("spawnexpedition", "spawnexpedition [spawn] [force] — FETCH a region-only species: a real round trip through the vanilla loader (party teleported to the donor region and back, a save on each leg), batch-caching every species that region donates. 'spawn' also spawns it on return; 'force' takes the trip even for a species an additive harvest could fetch with no loading screens (forensics only). The headless twin of the menu's trip buttons.", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; if (parts.Length <= 1) { Log.LogWarning((object)"[EXPEDITION] usage: spawnexpedition [spawn] [force]"); } else { int num = parts.Length; bool alsoSpawn = false; bool force = false; while (num > 1) { if (string.Equals(parts[num - 1], "spawn", StringComparison.OrdinalIgnoreCase)) { alsoSpawn = true; num--; } else { if (!string.Equals(parts[num - 1], "force", StringComparison.OrdinalIgnoreCase)) { break; } force = true; num--; } } string species = string.Join(" ", parts, 1, Math.Max(0, num - 1)).Trim(); if (species.Length == 0) { Log.LogWarning((object)"[EXPEDITION] usage: spawnexpedition [spawn] [force]"); } else { Spawner.Expedition(species, delegate(bool ok, string why) { if (!ok) { Log.LogWarning((object)("[EXPEDITION] '" + species + "': " + why + ".")); } else { Log.LogMessage((object)("[EXPEDITION] '" + species + "': " + why + ".")); if (alsoSpawn) { Spawner.Spawn(species, new SpawnOptions { OwnerTag = "spawnkit.verb" }, delegate(SpawnHandle h) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) Log.LogMessage((object)($"[EXPEDITION] post-trip spawn of '{species}': {h.State}" + ((h.State == SpawnState.Alive) ? "." : $" ({h.FailReason})."))); }); } } }, force); } } }, "[CMD]", false, true, true, (string)null, (ArgSpec[])null); val.Register("spawnmenu", "Toggle the in-game spawn menu (works regardless of the MenuKey; needs [Menu] EnableMenu=true).", (Action)delegate { SpawnMenu component = ((Component)this).GetComponent(); if ((Object)(object)component == (Object)null) { Log.LogWarning((object)"[MENU] EnableMenu=false — flip it in the .cfg and relaunch (menu component is boot-time)."); } else { component.Toggle(); } }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("lootprobe", "Corpse-loot diagnosis (V-TLOOT): a [LOOT] verdict line per tracked spawn — LootableOnDeath state, loot/skin dropper counts, pouch trigger. Probe a live spawn before killing, or a corpse after.", (Action)delegate { if (EnemySpawner.LootProbeAll() == 0) { Log.LogMessage((object)"[LOOT] no tracked spawns to probe (spawn one first; the probe also fires automatically at each spawn death)."); } }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("spawnlist", "List the merged donor-scene table (the spawnable species keys).", (Action)delegate { PrintSpawnList(); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("bonedump", "bonedump [name] — SkinnedMeshRenderer bone census for characters within 30m (the F2 vertical-line discriminator): internal/dead/external/null bones + rootBone per SMR. dead>0 on a spawn = bones destroyed with the donor scene; external>0 = bound outside the hierarchy.", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; string nameFilter = ((parts != null && parts.Length > 1) ? string.Join(" ", parts, 1, parts.Length - 1) : null); BoneProbe.Dump(nameFilter); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("ghostdiag", "ghostdiag [name] [fix] — [GHOSTDIAG] census for characters within 30m: visuals-holder init state, hitbox counts + layers, ragdoll hitbox colliders, attack transforms + weapon linecast sockets, LockingPoint reachability, live bone spread. 'fix' also runs the GhostRig re-init on each match and prints a second block, so before/after is one command. Invisible+unhittable+harmless all hang off this data.", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; bool fix = false; string nameFilter = null; if (parts != null && parts.Length > 1) { int num = parts.Length; if (string.Equals(parts[num - 1], "fix", StringComparison.OrdinalIgnoreCase)) { fix = true; num--; } if (num > 1) { nameFilter = string.Join(" ", parts, 1, num - 1); } } GhostDiag.DumpNearby(nameFilter, fix); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("ghostfix", "ghostfix [on|off] — BUG-GHOSTINERTSPAWN kill-switch, live: toggles [Visual] RigReinitPass (the mint-time hurtbox/ragdoll/attack re-init); no args = report. Applies to NEW mints only — use 'ghostdiag fix' to repair a body that already spawned.", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; if (parts != null && parts.Length > 1) { RigReinitPass.Value = !string.Equals(parts[1], "off", StringComparison.OrdinalIgnoreCase); } Log.LogMessage((object)$"[GHOSTFIX] ghostfix: RigReinitPass={RigReinitPass.Value} GhostDiag={GhostDiagnostics.Value}."); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("skcoopdump", "Co-op census on THIS machine (both roles): SpawnKit's mirror ack/fail reports + NetKit's Net.Dump() (transport/attach, hello ledger, PUN-signature + unknown-view tables, per-channel counters/ring), then the guest replica registry/queue.", (Action)delegate { Log.LogMessage((object)SpawnNet.Dump()); Log.LogMessage((object)GuestReplicas.Dump()); string text = EnemySpawner.PendingReleasesDump(); if (text.Length > 0) { Log.LogMessage((object)text); } }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("skwarmdump", "WARM MIRROR census (both roles): this machine's warm set / dead ends / remaining donor budget and when it last published, then every room peer's row — Unmodded (no compatible SpawnKit), HelloedNoRow (helloed, set not in yet), or Participating with its age, species list and budget — plus the room-wide intersection the gate spawns from and the WANT BOOK — every outstanding sk.want with its attempts, when the next send is due and when it is abandoned. Phase B/C: the gate is ENFORCED ([Coop] RoomWarmMode) and the want book drives the retries ([Coop] RoomWarmRequestTimeoutSeconds / MaxTimeoutSeconds / Attempts).", (Action)delegate { Log.LogMessage((object)WarmMirror.Dump()); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("skinject", "skinject — feed an encoded payload into the LOCAL receive path (synthetic sender, actor -1): exercises decode/queue/dedup/fail handling with no peer. Verbs: sk.spawn sk.gone sk.ack sk.fail sk.resync sk.test. Mirror MINT still refuses outside a guest session.", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; if (parts.Length < 2) { Log.LogWarning((object)"[SKNET] usage: skinject [payload]"); } else { string payload = ((parts.Length > 2) ? string.Join(" ", parts, 2, parts.Length - 2) : ""); SpawnNet.InjectLocal(parts[1], payload); } }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("skfail", "skfail [reason] — GUEST: send a real sk.fail for (drives the master's DespawnOnMirrorFailure leg). MASTER: injects it locally (same effect, no guest needed).", (Action)delegate(VerbContext ctx) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) string[] parts = ctx.Parts; if (parts.Length < 2) { Log.LogWarning((object)"[SKNET] usage: skfail [reason]"); } else { string uid = parts[1]; string reason = ((parts.Length > 2) ? string.Join(" ", parts, 2, parts.Length - 2) : "dev-forced"); string payload = SpawnNetProtocol.EncodeFail(new FailMsg { Uid = uid, Reason = reason }); if (PhotonNetwork.isNonMasterClientInRoom) { SpawnNet.SendToMaster("sk.fail", payload); } else { SpawnNet.InjectLocal("sk.fail", payload); } } }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("skdrop", "skdrop — GUEST: silently destroy replica WITHOUT telling the master (stages the loss races: master keeps streaming → unknown-view warns; 'skresync' should heal it).", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; if (parts.Length < 2) { Log.LogWarning((object)"[MIRROR] usage: skdrop "); } else if (!GuestReplicas.ForceDrop(parts[1])) { Log.LogWarning((object)("[MIRROR] skdrop: no replica with uid '" + parts[1] + "' (skcoopdump lists them).")); } }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("skresync", "Guest: send sk.resync — asks the master to re-flush its Alive spawns (the manual mirror-recovery verb; dedup makes replays harmless). Master: re-flushes to every peer.", (Action)delegate { SpawnNet.RequestResync(); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("skvisual", "skvisual [on|off] — MP §2/§2b kill-switch, live: toggles [Visual] PostActivationPass (the mint-time ForceVisuals/Rebind gate); no args = report. Applies to NEW mints only — already-spawned bodies keep whatever they got.", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; if (parts != null && parts.Length > 1) { PostActivationVisualPass.Value = !string.Equals(parts[1], "off", StringComparison.OrdinalIgnoreCase); } Log.LogMessage((object)$"[SPAWN] skvisual: PostActivationPass={PostActivationVisualPass.Value}."); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("skweapon", "skweapon [on|off] — MP §1 kill-switch, live: toggles [Coop] GuestEquipWeapon (guest-side weapon bind) AND [Coop] ShipWeaponIdentity (master-side hand identity on the wire) together; no args = report both. Flip off mid-session to bisect a bad interaction without a relaunch.", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; if (parts != null && parts.Length > 1) { bool value = !string.Equals(parts[1], "off", StringComparison.OrdinalIgnoreCase); GuestEquipWeapon.Value = value; ShipWeaponIdentity.Value = value; } Log.LogMessage((object)($"[SKNET] skweapon: GuestEquipWeapon={GuestEquipWeapon.Value} ShipWeaponIdentity={ShipWeaponIdentity.Value} " + "(guest bind applies from the next census/watch tick; identity applies to NEW announces only — already-frozen store bytes keep their fields).")); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("skgone", "skgone — MASTER: force-send an sk.gone (drives each guest teardown branch on demand). GUEST: injects it locally.", (Action)delegate(VerbContext ctx) { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) string[] parts = ctx.Parts; if (parts.Length < 3 || !Enum.TryParse(parts[2], ignoreCase: true, out GoneKind result)) { Log.LogWarning((object)"[SKNET] usage: skgone "); } else if (PhotonNetwork.isMasterClient && PhotonNetwork.inRoom) { SpawnNet.SendGone(parts[1], result); } else { SpawnNet.InjectLocal("sk.gone", SpawnNetProtocol.EncodeGone(new GoneMsg { Uid = parts[1], Kind = result })); } }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("skstream", "skstream [uid] — GUEST: per-replica stream health (serialize age, hp, posDelta vs the master's wanted position, drive-shape census + NCC gate reconstruction) — 'is the master actually driving this body'.", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; Log.LogMessage((object)GuestReplicas.StreamDump((parts.Length > 1) ? parts[1] : null)); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("skfollow", "skfollow — GUEST V22 discriminator: hard-snap replica onto the stream every frame, bypassing NetworkCharacterControl. Follows here but not vanilla = NCC gate problem (census names the false term); STILL pinned = an external writer owns the transform (sweep output_log.txt).", (Action)delegate(VerbContext ctx) { string[] parts = ctx.Parts; Log.LogMessage((object)GuestReplicas.SetFollow((parts.Length > 1) ? parts[1] : null)); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); val.Register("selftest", "Run the SpawnKit self-test ([SELFTEST] PASS/FAIL ... DONE).", (Action)delegate { SelfTest(); }, "[CMD]", false, true, false, (string)null, (ArgSpec[])null); CommonVerbs.RegisterAll(val, ModLog.op_Implicit(Log), new CommonVerbsOptions { ConfigSource = () => ((BaseUnityPlugin)this).Config }); } private static void ParseSpeciesAndCount(string[] args, out string species, out int count) { SpawnCountArgs val = SpawnCountArgs.Parse(args); species = val.Species; count = val.Count; } private static void PrintSpawnList() { Dictionary> donorScenes = DonorHarvest.DonorScenes; StringBuilder stringBuilder = new StringBuilder($"[SPAWNLIST] {donorScenes.Count} species in the donor table " + "(● mintable now · ◐ mintable here but cold on a peer · ○ cold here):"); foreach (KeyValuePair> item in donorScenes) { bool localWarm; try { localWarm = Spawner.CanMintNow(item.Key); } catch { localWarm = false; } stringBuilder.Append("\n " + WarmMirror.RoomGlyph(item.Key, localWarm) + " " + item.Key + " = " + string.Join(", ", item.Value)); } Log.LogMessage((object)stringBuilder.ToString()); } private void SelfTest() { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Invalid comparison between Unknown and I4 //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Invalid comparison between Unknown and I4 //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Invalid comparison between Unknown and I4 //IL_01c5: Unknown result type (might be due to invalid IL or missing references) //IL_01cb: Invalid comparison between Unknown and I4 //IL_01dd: Unknown result type (might be due to invalid IL or missing references) //IL_01e3: Invalid comparison between Unknown and I4 //IL_01f5: Unknown result type (might be due to invalid IL or missing references) //IL_01fb: Invalid comparison between Unknown and I4 //IL_020d: Unknown result type (might be due to invalid IL or missing references) //IL_0213: Invalid comparison between Unknown and I4 //IL_021c: Unknown result type (might be due to invalid IL or missing references) //IL_029a: Unknown result type (might be due to invalid IL or missing references) //IL_029c: Unknown result type (might be due to invalid IL or missing references) //IL_029d: Unknown result type (might be due to invalid IL or missing references) //IL_02b9: Unknown result type (might be due to invalid IL or missing references) //IL_02c0: Unknown result type (might be due to invalid IL or missing references) //IL_02cd: Unknown result type (might be due to invalid IL or missing references) //IL_02d4: Unknown result type (might be due to invalid IL or missing references) //IL_02e1: Unknown result type (might be due to invalid IL or missing references) //IL_02e8: Unknown result type (might be due to invalid IL or missing references) //IL_02f0: Unknown result type (might be due to invalid IL or missing references) //IL_02f7: 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_0407: Invalid comparison between Unknown and I4 //IL_0304: Unknown result type (might be due to invalid IL or missing references) //IL_030b: 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_0455: Invalid comparison between Unknown and I4 //IL_0313: Unknown result type (might be due to invalid IL or missing references) //IL_031a: Unknown result type (might be due to invalid IL or missing references) //IL_0322: Unknown result type (might be due to invalid IL or missing references) //IL_0329: Unknown result type (might be due to invalid IL or missing references) //IL_0331: Unknown result type (might be due to invalid IL or missing references) SelfTestHarness val = new SelfTestHarness(ModLog.op_Implicit(Log)); val.Begin("SpawnKit"); val.Check("uid mint prefixed+recognized", SpawnUid.IsSpawnUid(SpawnUid.Mint(Guid.NewGuid()))); IReadOnlyList<(float, float)> readOnlyList = RingPlacement.Candidates(0f, 1f, 4f, 16); val.Check("ring plan yields 16 candidates", readOnlyList.Count == 16); val.Check("ring first candidate straight ahead", Math.Abs(readOnlyList[0].Item1) < 0.001f && Math.Abs(readOnlyList[0].Item2 - 4f) < 0.001f); val.Check("donor table non-empty", DonorHarvest.DonorScenes.Count > 0); val.Check("kill-switch config bound", Enabled != null); val.Check("watch: alive+gone -> Despawned", (int)SpawnWatch.Next((WatchState)1, false, false) == 3); val.Check("watch: alive+dead -> Died", (int)SpawnWatch.Next((WatchState)1, true, false) == 2); val.Check("policy: distance request clamped", SpawnPolicy.EffectiveDistance((float?)500f, 5f) == 50f); val.Check("policy: null owner filter matches all", SpawnPolicy.MatchesOwner("anything", (string)null)); val.Check("policy: corpse defaults from config", (int)CorpseRules.EffectivePolicy((CorpsePolicy?)null, (CorpsePolicy)1) == 1); val.Check("policy: negative corpse linger clamped", CorpseRules.EffectiveLinger((float?)(-5f), 0f) == 0f); val.Check("policy: solo/offline never room-refused", !SpawnPolicy.RefuseRoomSpawn(true, 0, false)); val.Check("policy: host+guests room-refused by default", SpawnPolicy.RefuseRoomSpawn(true, 1, false)); val.Check("policy: AllowSpawnInRoom overrides the room gate", !SpawnPolicy.RefuseRoomSpawn(true, 1, true)); val.Check("coop: solo room decides Solo", (int)SpawnPolicy.DecideRoomSpawn(true, 0, true, 0, false) == 0); val.Check("coop: all peers helloed decides CoopReady", (int)SpawnPolicy.DecideRoomSpawn(true, 2, true, 0, false) == 1); val.Check("coop: un-helloed peer refuses PeersNotReady", (int)SpawnPolicy.DecideRoomSpawn(true, 2, true, 1, false) == 4); val.Check("coop: kill-switch off = legacy refusal", (int)SpawnPolicy.DecideRoomSpawn(true, 1, false, 0, false) == 5); SpawnMsg val2 = new SpawnMsg { Proto = 1, SpeciesKey = "Alpha; Tuano\\saur", Uid = "SK_selftest", ViewId = 24001, Scene = "ChersoneseNewTerrain", X = 1.5f, Y = -2.25f, Z = 300.125f, YawDeg = 271.5f, Faction = 4, StripQuestEvents = true }; SpawnMsg val3 = val2; SpawnMsg val4 = default(SpawnMsg); bool flag = SpawnNetProtocol.TryDecodeSpawn(SpawnNetProtocol.EncodeSpawn(val3), ref val4); val.Check("coop: spawn codec round-trips (incl. ';'/'\\' escaping)", flag && val4.SpeciesKey == val3.SpeciesKey && val4.Uid == val3.Uid && val4.ViewId == val3.ViewId && val4.Scene == val3.Scene && val4.X == val3.X && val4.YawDeg == val3.YawDeg && val4.Faction == val3.Faction && val4.StripQuestEvents); val.Check("coop: malformed spawn payload refused", !SpawnNetProtocol.TryDecodeSpawn("1;garbage", ref val2)); if (PhotonNetwork.inRoom) { SpawnNet.LastTestPayload = null; bool flag2 = SpawnNet.SendToAllLoopback("sk.test", "loopback-proof"); val.Check("coop: sk-channel loopback dispatched through NetKit's transport", flag2 && SpawnNet.LastTestPayload == "loopback-proof"); val.Check("coop: NetKit transport attached (NK_Bus on CharacterManager's view)", Net.Attached); } val.Check("no cross-mod keybind conflicts (ForgeKit.Keybinds)", !Keybinds.HasConflicts()); val.Check("facade: species list non-empty", Spawner.SpeciesKeys().Count > 0); if ((Object)(object)LocalPlayer == (Object)null) { SpawnHandle spawnHandle = Spawner.Spawn("Hyena"); val.Check("facade: no-player spawn -> Failed/NoPlayer", spawnHandle.State == SpawnState.Failed && (int)spawnHandle.FailReason == 4); } SpawnHandle spawnHandle2 = Spawner.Spawn("", null, delegate { throw new InvalidOperationException("selftest probe"); }); val.Check("facade: empty species -> sync Failed/UnknownSpecies + throwing callback isolated (see [SPAWN] error above)", spawnHandle2.State == SpawnState.Failed && (int)spawnHandle2.FailReason == 7); val.Check("CharacterManager present", (Object)(object)CharacterManager.Instance != (Object)null); val.Check("local player present", (Object)(object)LocalPlayer != (Object)null); val.Check("AISquadManager present (gameplay scene)", (Object)(object)AISquadManager.Instance != (Object)null); val.Done(); } } internal static class ReplicaDrive { internal static ShapeCensus Census(GameObject go) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0197: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Unknown result type (might be due to invalid IL or missing references) ShapeCensus result = default(ShapeCensus); try { result.HasCharacterAI = (Object)(object)go.GetComponent() != (Object)null; NetworkCharacterControl[] components = go.GetComponents(); result.NccCount = components.Length; if (components.Length != 0 && (Object)(object)components[0] != (Object)null) { result.NccEnabled = ((Behaviour)components[0]).enabled; result.CloseToPlayer = ((CharacterControl)components[0]).CloseToPlayer; } result.HasCharAIDisable = (Object)(object)go.GetComponent() != (Object)null; NavMeshAgent component = go.GetComponent(); result.AgentPresent = (Object)(object)component != (Object)null; if ((Object)(object)component != (Object)null) { result.AgentEnabled = ((Behaviour)component).enabled; result.AgentUpdatePosition = component.updatePosition; result.AgentUpdateRotation = component.updateRotation; } int num = 0; AIRoot[] componentsInChildren = go.GetComponentsInChildren(true); foreach (AIRoot val in componentsInChildren) { if ((Object)(object)val != (Object)null && ((Component)val).gameObject.activeSelf) { num++; } } result.ActiveAiRoots = num; CharacterController component2 = go.GetComponent(); result.CcPresent = (Object)(object)component2 != (Object)null; if ((Object)(object)component2 != (Object)null) { result.CcEnabled = ((Collider)component2).enabled; } PhotonView component3 = go.GetComponent(); result.SyncMode = (ViewSyncMode)(((Object)(object)component3 != (Object)null) ? ((int)MapSync(component3.synchronization)) : 0); Character component4 = go.GetComponent(); if ((Object)(object)component4 != (Object)null) { try { result.IsAI = component4.IsAI; } catch { } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[MIRROR] shape census threw: " + ex.Message)); } return result; } private static ViewSyncMode MapSync(ViewSynchronization s) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Invalid comparison between Unknown and I4 if ((int)s != 0) { if ((int)s == 2) { return (ViewSyncMode)2; } return (ViewSyncMode)3; } return (ViewSyncMode)1; } internal static string NudgeInit(Character ch, int attempts) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Invalid comparison between Unknown and I4 //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Invalid comparison between Unknown and I4 try { StartingEquipment component = ((Component)ch).GetComponent(); bool flag = false; try { flag = ch.m_equipmentInit; } catch { } InitNudge val = ReplicaShape.DecideInitNudge(ch.Initialized, flag, (Object)(object)component != (Object)null, attempts); if ((int)val != 1) { if ((int)val == 2) { ch.EquipmentInitDone(); return "equip-flag-direct"; } return null; } if (!((Behaviour)component).enabled) { ((Behaviour)component).enabled = true; } component.InitWanted(); return "equip-init-wanted"; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[MIRROR] init nudge threw: " + ex.Message)); return "THREW"; } } internal static string Apply(GameObject go, ShapePlan plan) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Invalid comparison between Unknown and I4 //IL_0072: 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) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Invalid comparison between Unknown and I4 //IL_018c: Unknown result type (might be due to invalid IL or missing references) //IL_018d: Unknown result type (might be due to invalid IL or missing references) //IL_0194: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) //IL_0147: 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_01cd: Unknown result type (might be due to invalid IL or missing references) //IL_01d4: Unknown result type (might be due to invalid IL or missing references) //IL_0166: 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_016d: Unknown result type (might be due to invalid IL or missing references) //IL_023f: Unknown result type (might be due to invalid IL or missing references) //IL_0240: Unknown result type (might be due to invalid IL or missing references) //IL_0247: Unknown result type (might be due to invalid IL or missing references) //IL_0278: Unknown result type (might be due to invalid IL or missing references) //IL_0279: Unknown result type (might be due to invalid IL or missing references) //IL_0283: Unknown result type (might be due to invalid IL or missing references) //IL_029a: Unknown result type (might be due to invalid IL or missing references) //IL_02a0: Invalid comparison between Unknown and I4 //IL_02a4: Unknown result type (might be due to invalid IL or missing references) //IL_02a9: Unknown result type (might be due to invalid IL or missing references) //IL_02ae: Unknown result type (might be due to invalid IL or missing references) //IL_02b9: Unknown result type (might be due to invalid IL or missing references) List list = new List(); try { if ((plan.Fixes & 0x80) != 0) { CharacterAI component = go.GetComponent(); if ((Object)(object)component != (Object)null) { try { if ((Object)(object)component.m_aiStatesRoot != (Object)null) { ((Component)component.m_aiStatesRoot).gameObject.SetActive(false); } } catch { } try { component.m_aiStatesRoot = null; } catch { } Object.DestroyImmediate((Object)(object)component); list.Add("charAI-killed"); } } bool flag = (plan.Fixes & 2) > 0; if ((plan.Fixes & 1) != 0) { NetworkCharacterControl[] components = go.GetComponents(); int num = ((flag || (Object)(object)go.GetComponent() != (Object)null) ? components.Length : (components.Length - 1)); for (int i = 0; i < components.Length && i < num; i++) { if ((Object)(object)components[i] != (Object)null) { Object.DestroyImmediate((Object)(object)components[i]); } } list.Add($"ncc-killed({Math.Max(num, 0)})"); } if (flag) { NetworkCharacterControl val = go.AddComponent(); ((CharacterControl)val).CloseToPlayer = (plan.Fixes & 8) > 0; list.Add("ncc-added" + (((CharacterControl)val).CloseToPlayer ? "+ctp" : "")); } else { NetworkCharacterControl component2 = go.GetComponent(); if ((Object)(object)component2 != (Object)null) { if ((plan.Fixes & 4) != 0 && !((Behaviour)component2).enabled) { ((Behaviour)component2).enabled = true; list.Add("ncc-enabled"); } if ((plan.Fixes & 8) != 0 && !((CharacterControl)component2).CloseToPlayer) { ((CharacterControl)component2).CloseToPlayer = true; list.Add("ctp-forced"); } } } if ((plan.Fixes & 0x10) != 0) { NavMeshAgent component3 = go.GetComponent(); if ((Object)(object)component3 != (Object)null) { component3.updatePosition = false; component3.updateRotation = false; ((Behaviour)component3).enabled = false; list.Add("agent-off"); } } if ((plan.Fixes & 0x20) != 0) { int num2 = 0; AIRoot[] componentsInChildren = go.GetComponentsInChildren(true); foreach (AIRoot val2 in componentsInChildren) { if ((Object)(object)val2 != (Object)null && ((Component)val2).gameObject.activeSelf) { ((Component)val2).gameObject.SetActive(false); num2++; } } list.Add($"aiRoots-off({num2})"); } if ((plan.Fixes & 0x40) != 0) { CharacterController component4 = go.GetComponent(); if ((Object)(object)component4 != (Object)null && !((Collider)component4).enabled) { ((Collider)component4).enabled = true; list.Add("cc-on"); } } if ((plan.Fixes & 0x100) != 0) { PhotonView component5 = go.GetComponent(); if ((Object)(object)component5 != (Object)null && (int)component5.synchronization != 2) { ViewSynchronization synchronization = component5.synchronization; component5.synchronization = (ViewSynchronization)2; list.Add($"sync-unreliable(was {synchronization})"); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[MIRROR] shape apply threw after [" + string.Join(",", list) + "]: " + ex.Message)); list.Add("THREW"); } if (list.Count != 0) { return string.Join(",", list); } return "none"; } } public enum MirrorState { Mirroring, Active, Dead } public struct ReplicaInfo { public string Uid; public string SpeciesKey; public MirrorState State; public int ViewId; public string ConsumerData; } internal static class SpawnDisengage { internal static void DisengageSpawn(RemovalReason reason, Character spawn) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) if (!DisengagePolicy.NeedsDisengage(reason)) { return; } CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null) { return; } bool flag = DisengagePolicy.ByReference((Object)(object)spawn != (Object)null); int num = 0; int num2 = 0; List list = instance.Characters?.Values; if (list != null) { Character[] array = list.ToArray(); foreach (Character val in array) { if ((Object)(object)val == (Object)null || val == spawn) { continue; } List list2 = null; try { list2 = val.EngagedCharacters; } catch { } if (list2 == null || list2.Count == 0) { continue; } if (!flag) { if (EngagementHygiene.SweepStale(val, $"spawn {reason}") > 0) { num++; } continue; } bool flag2 = false; for (int num3 = list2.Count - 1; num3 >= 0; num3--) { if (list2[num3] == spawn) { if (EngagementHygiene.Remove(val, spawn)) { flag2 = true; } else { num2++; Plugin.Log.LogWarning((object)("[SPAWN] could not clear '" + val.Name + "'s engagement with the removed spawn — it stays engaged with a corpse (permanent combat music until something else clears it). Any [COMBATFIX] warning above names the underlying failure.")); } } } if (flag2) { num++; } } } if (flag && (Object)(object)spawn != (Object)null) { try { spawn.ResetCombat(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SPAWN] ResetCombat on the removed spawn threw (" + ex.GetType().Name + ": " + ex.Message + ").")); } } if (num > 0 || num2 > 0) { Plugin.Log.LogMessage((object)($"[SPAWN] disengaged {num} character(s) on {reason} (uid was tracked)" + ((num2 > 0) ? $" — {num2} engagement(s) FAILED to clear (see the warnings above)." : "."))); } } } public static class Spawner { private static bool _warnedGuestRequestRoomWarm; public static bool IsExpeditionRunning { get { if (!ExpeditionRun.InProgress) { return ExpeditionHarvest.InProgress; } return true; } } public static event Action OnPeerSceneReady; public static event Action OnMirrored; public static SpawnHandle Spawn(string speciesKey, SpawnOptions options = null, Action onReady = null) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) options = options ?? new SpawnOptions(); SpawnHandle spawnHandle = new SpawnHandle(speciesKey?.Trim() ?? "", options.OwnerTag); if (onReady != null) { spawnHandle.OnReady += onReady; } if (spawnHandle.SpeciesKey.Length == 0) { spawnHandle.Resolve(SpawnState.Failed, (FailReason)7); return spawnHandle; } FailReason val = EnemySpawner.Preflight(spawnHandle, options); if ((int)val != 0) { spawnHandle.Resolve(SpawnState.Failed, val); return spawnHandle; } try { ((MonoBehaviour)Plugin.Instance).StartCoroutine(EnemySpawner.SpawnRoutine(spawnHandle, options)); } catch (Exception arg) { ModLog log = Plugin.Log; if (log != null) { log.LogError((object)("[SPAWN] could not start the spawn pipeline for '" + spawnHandle.SpeciesKey + "' " + $"(owner '{spawnHandle.OwnerTag}') — the handle is failed and its cap slot released: {arg}")); } EnemySpawner.Despawn(spawnHandle, kill: false); } return spawnHandle; } public static void Despawn(SpawnHandle handle, bool kill = false) { EnemySpawner.Despawn(handle, kill); } public static void DespawnAll(string ownerTag = null, bool kill = false) { EnemySpawner.DespawnAll(ownerTag, kill); } public static void Prewarm(string speciesKey, Action onDone = null) { if ((Object)(object)Plugin.Instance == (Object)null) { onDone?.Invoke(obj: false); return; } string text = speciesKey?.Trim() ?? ""; if (PhotonNetwork.isNonMasterClientInRoom) { if (EnemySpawner.RefusedNonMasterHarvest("[TEMPLATE] prewarm '" + text + "'", allowVoluntary: true)) { onDone?.Invoke(obj: false); } else { GuestReplicas.PrewarmVoluntary(text, onDone); } } else { ((MonoBehaviour)Plugin.Instance).StartCoroutine(SpawnTemplates.Acquire(text, delegate(GameObject t) { onDone?.Invoke((Object)(object)t != (Object)null); })); } } public static string GuestWarmStatus(string speciesKey) { return GuestReplicas.VoluntaryStatus(speciesKey); } public static string GuestWarmCounter() { return GuestReplicas.VoluntaryCounter(); } internal static void RaisePeerSceneReady(int actor) { Action onPeerSceneReady = Spawner.OnPeerSceneReady; if (onPeerSceneReady == null) { return; } Delegate[] invocationList = onPeerSceneReady.GetInvocationList(); foreach (Delegate obj in invocationList) { try { ((Action)obj)(actor); } catch (Exception arg) { ModLog log = Plugin.Log; if (log != null) { log.LogError((object)$"[MIRROR] consumer OnPeerSceneReady callback threw for actor {actor}: {arg}"); } } } } public static bool IsPrewarmed(string speciesKey) { return SpawnTemplates.IsCached(speciesKey); } public static bool IsExpeditionCached(string speciesKey) { string text = speciesKey?.Trim() ?? ""; if (text.Length == 0) { return false; } try { BodyTemplate val = default(BodyTemplate); return BodyTemplateCache.TryResolveExact(text, ref val) && (Object)(object)((val != null) ? val.Dormant : null) != (Object)null && SpawnTemplates.IsAdoptable(val.Dormant); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[TEMPLATE] expedition-cache probe for '" + text + "' threw: " + ex.Message)); return false; } } public static bool CanMintNow(string speciesKey) { if (!IsPrewarmed(speciesKey)) { return IsExpeditionCached(speciesKey); } return true; } public static void Expedition(string speciesKey, Action onDone = null, bool force = false) { ExpeditionRun.ForSpecies(speciesKey, onDone, force); } public static string ResolvedDonorName(string speciesKey) { return SpawnTemplates.ResolvedDonorName(speciesKey); } public static bool IsExpeditionOnly(string speciesKey) { Dictionary> donorScenes = DonorHarvest.DonorScenes; string text = default(string); List list = default(List); if (!SpeciesTable.TryResolveKey>(donorScenes, speciesKey?.Trim() ?? "", ref text, ref list, (string)null) || list == null || list.Count == 0) { return false; } List list2 = default(List); return DonorTable.FilterViable((IEnumerable)list, ref list2).Count == 0; } public static IReadOnlyList Active(string ownerTag = null) { return EnemySpawner.Snapshot(ownerTag); } public static void Active(string ownerTag, List into) { EnemySpawner.Snapshot(ownerTag, into); } public static IReadOnlyList Replicas() { return GuestReplicas.SnapshotInfos(); } internal static void RaiseMirrored(ReplicaInfo info) { Action onMirrored = Spawner.OnMirrored; if (onMirrored == null) { return; } Delegate[] invocationList = onMirrored.GetInvocationList(); foreach (Delegate obj in invocationList) { try { ((Action)obj)(info); } catch (Exception arg) { ModLog log = Plugin.Log; if (log != null) { log.LogError((object)("[MIRROR] consumer OnMirrored callback threw for " + $"'{info.SpeciesKey}' (uid {info.Uid}): {arg}")); } } } } public static RoomWarmDecision RoomWarmDecision(string speciesKey) { //IL_0031: 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) string text = speciesKey?.Trim() ?? ""; return SpeciesRoomPolicy.Decide(text, CanMintNow(text), (IReadOnlyList)WarmMirror.PeerRows(), (RoomWarmMode)((Plugin.RoomWarmMode != null) ? ((int)Plugin.RoomWarmMode.Value) : 0)); } public static bool CanMintRoomWide(string speciesKey) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) return !SpeciesRoomPolicy.IsVeto(RoomWarmDecision(speciesKey)); } public static List RoomWarmSnapshot() { return SpeciesRoomPolicy.Intersection((IEnumerable)WarmMirror.LocalWarmKeys(), (IReadOnlyList)WarmMirror.PeerRows()); } public static int RequestRoomWarm(IList prioritySpecies) { //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) if (prioritySpecies == null || prioritySpecies.Count == 0) { return 0; } if ((Object)(object)Plugin.Instance == (Object)null || Plugin.Log == null) { return 0; } if (!SpawnNet.IsRoomHostRaw()) { if (!_warnedGuestRequestRoomWarm) { _warnedGuestRequestRoomWarm = true; Plugin.Log.LogWarning((object)"[MIRROR] RequestRoomWarm ignored on a guest (warned once per session): only the master may spend another machine's donor-harvest budget — the sk.want handler refuses a non-master sender anyway. NB this asks RAW authority, so a SplitScene-flipped guest lands here too: the flip is for local simulation, not for spending a peer's budget."); } return 0; } List list = new List(prioritySpecies); int num = ((Plugin.RoomWarmRequestCap != null) ? Plugin.RoomWarmRequestCap.Value : 3); List list2 = RoomWarmRequest.Plan((IReadOnlyList)list, (IReadOnlyList)WarmMirror.PeerRows(), num, (Func)WarmMirror.IsAbandoned); if (list2.Count == 0) { return 0; } for (int i = 0; i < list2.Count; i++) { WarmMirror.WantSpeciesOn(list2[i].Actor, list2[i].Species); } return WarmMirror.DriveWants(); } public static IReadOnlyList SpeciesKeys() { List list = new List(DonorHarvest.DonorScenes.Keys); list.Sort(StringComparer.OrdinalIgnoreCase); return list; } } public enum SpawnState { Pending, Alive, Died, Despawned, Failed } public sealed class SpawnHandle { internal int MintFrame; private Action _onReady; internal bool RoomGateOk; internal bool RoomGateEvaluated; private Dictionary _coldUnsafeReports; internal int ViewId = -1; internal int CoopFaction = -1; internal bool CoopStripQuestEvents = true; internal CorpsePolicy CorpsePolicy; internal float CorpseLingerSeconds; public SpawnState State { get; internal set; } public FailReason FailReason { get; internal set; } public string SpeciesKey { get; } public string OwnerTag { get; } public string Uid { get; internal set; } public Character Character { get; internal set; } public bool IsAlive { get { if (State == SpawnState.Alive) { return (Object)(object)Character != (Object)null; } return false; } } internal int ColdUnsafeReportsTotal { get { if (_coldUnsafeReports == null) { return 0; } int num = 0; foreach (KeyValuePair coldUnsafeReport in _coldUnsafeReports) { num += coldUnsafeReport.Value; } return num; } } public string ConsumerData { get; internal set; } = ""; public event Action OnReady { add { if (State != SpawnState.Pending) { Invoke(value, "OnReady(late)"); } else { _onReady = (Action)Delegate.Combine(_onReady, value); } } remove { _onReady = (Action)Delegate.Remove(_onReady, value); } } public event Action OnDied; public event Action OnDespawned; internal int NoteColdUnsafeFrom(int actor) { if (_coldUnsafeReports == null) { _coldUnsafeReports = new Dictionary(); } _coldUnsafeReports.TryGetValue(actor, out var value); _coldUnsafeReports[actor] = value + 1; return value; } internal SpawnHandle(string speciesKey, string ownerTag) { SpeciesKey = speciesKey; OwnerTag = SpawnPolicy.NormalizeOwnerTag(ownerTag); } internal void Resolve(SpawnState state, FailReason reason = (FailReason)0) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) if (State == SpawnState.Pending) { State = state; FailReason = reason; Action onReady = _onReady; _onReady = null; FireAll(onReady, "OnReady"); } } internal void FireDied() { FireAll(this.OnDied, "OnDied"); this.OnDied = null; this.OnDespawned = null; } internal void FireDespawned() { FireAll(this.OnDespawned, "OnDespawned"); this.OnDied = null; this.OnDespawned = null; } private void FireAll(Action handlers, string label) { if (handlers != null) { Delegate[] invocationList = handlers.GetInvocationList(); foreach (Delegate obj in invocationList) { Invoke((Action)obj, label); } } } private void Invoke(Action handler, string label) { try { handler?.Invoke(this); } catch (Exception ex) { ModLog log = Plugin.Log; if (log != null) { log.LogError((object)$"[SPAWN] consumer {label} callback threw for '{SpeciesKey}' (owner '{OwnerTag}'): {ex}"); } } } } internal class SpawnMenu : MonoBehaviour { private const float ArmWindowSeconds = 5f; private bool _open; private Rect _rect = new Rect(80f, 80f, 420f, 480f); private Vector2 _scroll; private int _count = 1; private string _status = ""; private IReadOnlyList _species = Array.Empty(); private string _filter = ""; private IReadOnlyList _filtered = Array.Empty(); private Arm _arm; private bool _closeRequested; private bool _tripInFlight; private bool _nonMaster; private bool _guestCanWarm = true; public void Toggle() { SetOpen(!_open); } private void SetOpen(bool open) { if (open != _open) { _open = open; if (_open) { _species = Spawner.SpeciesKeys(); _filter = ""; _filtered = _species; } CursorControl.SetMenuOpen(_open); } } private void Update() { //IL_006e: 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 (_closeRequested) { _closeRequested = false; SetOpen(open: false); } _tripInFlight = Spawner.IsExpeditionRunning; _nonMaster = PhotonNetwork.isNonMasterClientInRoom; _guestCanWarm = !_nonMaster || (Plugin.GuestPrewarm != null && Plugin.GuestPrewarm.Value); if (_tripInFlight) { if (_open) { SetOpen(open: false); } return; } KeyboardShortcut value = Plugin.MenuKey.Value; if (((KeyboardShortcut)(ref value)).IsDown() && MenuCursorPolicy.CanOpen(CursorControl.VanillaMenuFocused)) { Toggle(); } bool flag = SpeciesFilter.CancelDown(Input.GetKeyDown((KeyCode)27), CancelInput.Down()); if (SpeciesFilter.EscConsumesFilter(_open, _filter.Length > 0, flag)) { _filter = ""; } else if (MenuCursorPolicy.CloseOnEsc(_open, flag)) { SetOpen(open: false); } _filtered = SpeciesFilter.Apply(_species, _filter, (Func)Spawner.ResolvedDonorName); if (MenuCursorPolicy.ForceClose(_open, CursorControl.VanillaMenuFocused)) { SetOpen(open: false); } } private void OnDisable() { SetOpen(open: false); } private void OnGUI() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Expected O, but got Unknown //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) if (_open) { _rect = GUILayout.Window("cobalt.spawnkit".GetHashCode(), _rect, new WindowFunction(DrawWindow), $"SpawnKit — {Plugin.MenuKey.Value} closes", Array.Empty()); } } private static string OwnerBreakdownOf(IReadOnlyList handles) { List list = new List(handles.Count); for (int i = 0; i < handles.Count; i++) { list.Add(handles[i].OwnerTag); } return SpawnMenuLabels.OwnerBreakdown((IEnumerable)list); } private void DrawWindow(int id) { //IL_0254: Unknown result type (might be due to invalid IL or missing references) //IL_025e: Unknown result type (might be due to invalid IL or missing references) //IL_0263: Unknown result type (might be due to invalid IL or missing references) //IL_0292: Unknown result type (might be due to invalid IL or missing references) //IL_02b1: Unknown result type (might be due to invalid IL or missing references) //IL_02b6: Unknown result type (might be due to invalid IL or missing references) //IL_02d8: Unknown result type (might be due to invalid IL or missing references) //IL_036f: Unknown result type (might be due to invalid IL or missing references) //IL_037e: Unknown result type (might be due to invalid IL or missing references) //IL_0302: Unknown result type (might be due to invalid IL or missing references) //IL_02e1: Unknown result type (might be due to invalid IL or missing references) //IL_042e: Unknown result type (might be due to invalid IL or missing references) //IL_0444: Unknown result type (might be due to invalid IL or missing references) //IL_0475: Unknown result type (might be due to invalid IL or missing references) //IL_0481: 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_04b9: Unknown result type (might be due to invalid IL or missing references) //IL_04c3: Unknown result type (might be due to invalid IL or missing references) //IL_04db: Unknown result type (might be due to invalid IL or missing references) //IL_04e0: Unknown result type (might be due to invalid IL or missing references) //IL_04e3: Unknown result type (might be due to invalid IL or missing references) //IL_04e5: Unknown result type (might be due to invalid IL or missing references) //IL_04ea: Unknown result type (might be due to invalid IL or missing references) //IL_04ef: Unknown result type (might be due to invalid IL or missing references) int num = Math.Max(1, Plugin.MaxActiveSpawns.Value); IReadOnlyList readOnlyList = Spawner.Active(); int count = readOnlyList.Count; GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label($"Active: {count}/{num}", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(90f) }); GUILayout.FlexibleSpace(); GUILayout.Label("Count:", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(45f) }); if (GUILayout.Button("-", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(24f) })) { _count = Math.Max(1, _count - 1); } GUILayout.Label(_count.ToString(), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(20f) }); if (GUILayout.Button("+", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(24f) })) { _count = Math.Min(num, _count + 1); } GUILayout.EndHorizontal(); string text = OwnerBreakdownOf(readOnlyList); if (text.Length > 0) { GUILayout.Label(" by owner: " + text, Array.Empty()); } GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button("Despawn All (silent)", Array.Empty())) { Spawner.DespawnAll(); } if (GUILayout.Button("Kill All (loot)", Array.Empty())) { Spawner.DespawnAll(null, kill: true); } GUILayout.EndHorizontal(); IReadOnlyList filtered = _filtered; GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("Search:", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(55f) }); _filter = GUILayout.TextField(_filter, Array.Empty()); if (GUILayout.Button("×", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(24f) })) { _filter = ""; } if (filtered != _species) { GUILayout.Label($"{filtered.Count}/{_species.Count}", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(60f) }); } GUILayout.EndHorizontal(); bool tripInFlight = _tripInFlight; bool nonMaster = _nonMaster; bool guestCanWarm = _guestCanWarm; bool value = Plugin.EnableExpeditions.Value; float unscaledTime = Time.unscaledTime; _scroll = GUILayout.BeginScrollView(_scroll, Array.Empty()); foreach (string item in filtered) { bool flag = Spawner.CanMintNow(item); bool flag2 = Spawner.IsExpeditionOnly(item); bool flag3 = ExpeditionRow.IsArmed(_arm, item, unscaledTime, 5f); Buttons val = ExpeditionRow.For(flag, flag2, value, tripInFlight, flag3); string text2 = SpawnMenuLabels.RowLabel(item, Spawner.ResolvedDonorName(item), flag2 && !flag); if (nonMaster && (val.ShowWarmTrip || (val.ShowPrewarm && !guestCanWarm))) { text2 += " (host only)"; } else if (nonMaster && val.ShowPrewarm) { string text3 = Spawner.GuestWarmStatus(item); if (text3.Length > 0) { text2 = text2 + " (" + text3 + ")"; } } GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label(WarmMirror.RoomGlyph(item, flag), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(18f) }); GUILayout.Label(text2, Array.Empty()); GUILayout.FlexibleSpace(); GUI.enabled = val.Interactive && guestCanWarm; if (val.ShowPrewarm && GUILayout.Button("Prewarm", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(70f) })) { string k = item; _status = "Prewarming " + k + "…"; Spawner.Prewarm(k, delegate(bool ok) { _status = (ok ? (k + " template resident.") : ("Prewarm " + k + " FAILED (see log).")); }); if (nonMaster) { string text4 = Spawner.GuestWarmStatus(k); if (text4.Length > 0) { _status = k + " " + text4; } } } GUI.enabled = val.Interactive && !nonMaster; if (val.ShowWarmTrip && GUILayout.Button("Warm (trip)", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(90f) })) { BeginTrip(item, 0); } GUI.enabled = val.Interactive; if (GUILayout.Button(val.SpawnLabel, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(val.SpawnCostsATrip ? 90f : 60f) })) { string text5 = item; if (val.SpawnCostsATrip) { ClickResult val2 = ExpeditionRow.Click(_arm, text5, unscaledTime, 5f, Plugin.ConfirmMenuExpedition.Value); _arm = val2.State; if (val2.Fire) { BeginTrip(text5, _count); } else { _status = text5 + " needs an EXPEDITION: two loading screens there and back, and the game saves on each leg. Click Confirm to go (it also caches every other species of that region)."; } } else { _status = (flag ? $"Spawning {_count}x {text5}…" : $"Spawning {_count}x {text5} (cold — donor harvest first)…"); for (int num2 = 0; num2 < _count; num2++) { SpawnOne(text5, toast: false); } } } GUI.enabled = true; GUILayout.EndHorizontal(); } GUILayout.EndScrollView(); if (_status.Length > 0) { GUILayout.Label(_status, Array.Empty()); } if (nonMaster && guestCanWarm) { GUILayout.Label(Spawner.GuestWarmCounter(), Array.Empty()); } GUI.DragWindow(); } private void BeginTrip(string key, int spawnCount) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) int count = Mathf.Max(0, spawnCount); _arm = default(Arm); _closeRequested = true; Notify((count > 0) ? ("Expedition for " + key + " — there and back, then it appears.") : ("Expedition for " + key + " — fetching a body.")); Spawner.Expedition(key, delegate(bool ok, string why) { if (!ok) { _status = key + ": expedition FAILED — " + why + "."; Notify("Expedition for " + key + " failed: " + why + "."); } else if (count == 0) { _status = key + ": " + why + "."; Notify(key + ": body template cached — it spawns instantly now."); } else { _status = $"Spawning {count}x {key} (home from the expedition)…"; for (int i = 0; i < count; i++) { SpawnOne(key, toast: true); } } }); } private void SpawnOne(string key, bool toast) { Spawner.Spawn(key, new SpawnOptions { OwnerTag = "spawnkit.menu" }, delegate(SpawnHandle h) { //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) bool flag = h.State == SpawnState.Alive; string text = (((Object)(object)h.Character != (Object)null && SpawnMenuLabels.IsDonorMismatch(h.SpeciesKey, h.Character.Name)) ? (h.SpeciesKey + " spawned as '" + h.Character.Name + "'.") : (h.SpeciesKey + " spawned.")); _status = (flag ? text : $"{h.SpeciesKey}: {h.State} ({h.FailReason})."); if (toast) { Notify(flag ? text : $"{h.SpeciesKey} could NOT be spawned ({h.FailReason}) — the expedition brought no body for it. See the log."); } }); } private static void Notify(string message) { Plugin.Log.LogMessage((object)("[EXPEDITION] " + message)); try { Notify.Player(Lifecycle.FirstLocalCharacterOrNull(), message); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[EXPEDITION] toast failed: " + ex.Message)); } } } internal static class SpawnNet { private sealed class MirrorReport { public readonly List Lines = new List(); } public const string ChannelId = "sk"; public const string LogTag = "SKNET"; private static NetChannel _channel; private static ReplicatedStore _store; internal static string LastTestPayload; private const int MaxFlushedPerActor = 128; private static readonly Dictionary> _flushedTo = new Dictionary>(); private static readonly ForgivenLedger _forgiven = new ForgivenLedger(); private const float ForgivenSweepSeconds = 2f; private static float _nextForgivenSweepAt; private const int MaxLedgerUids = 64; private const int MaxLedgerLinesPerUid = 32; private static readonly Dictionary _mirrorReports = new Dictionary(); private static readonly List _mirrorReportOrder = new List(); private static NetChannel Channel => _channel ?? (_channel = EnsureChannel()); internal static bool Attached => Net.Attached; private static ForgivenLedger Forgiven => _forgiven; private static NetChannel EnsureChannel() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Expected O, but got Unknown //IL_004a: 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_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Expected O, but got Unknown //IL_0095: Expected O, but got Unknown ChannelOptions val = new ChannelOptions(); val.LogTag = "SKNET"; val.HeartbeatFragment = HeartbeatFragment; NetChannel ch = Net.RegisterChannel("sk", "0.5.2", val); _store = ch.RegisterStore("spawn", new StoreOptions { Authority = (StoreAuthority)1, RefreshSeconds = 0f, FlushOnPeerReady = false, ClearOnRoomChange = true, Verbs = new StoreVerbs { Announce = "sk.spawn", Release = "sk.gone" } }); _store.OnSet += OnSpawnRecordSet; _store.OnCleared += OnSpawnRecordCleared; ch.Register("sk.corpse", (Action)delegate(NetMessage m) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) GuestReplicas.OnGoneMessage(m.Payload, m.SenderActor, "sk.corpse"); }, (HandlerRole)4); ch.Register("sk.ack", (Action)delegate(NetMessage m) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) OnAck(m.Payload, m.SenderActor); }, (HandlerRole)9); ch.Register("sk.fail", (Action)delegate(NetMessage m) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) OnFail(m.Payload, m.SenderActor); }, (HandlerRole)9); ch.Register("sk.resync", (Action)delegate(NetMessage m) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) OnResyncRequest(m.SenderActor); }); ch.Register("sk.test", (Action)delegate(NetMessage m) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) LastTestPayload = m.Payload; }); WarmMirror.Attach(ch); ch.Register("sk.want", (Action)delegate(NetMessage m) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Invalid comparison between Unknown and I4 string text = (m.Payload ?? "").Trim(); if (text.Length == 0) { CountDrop("sk.want", "empty-key"); } else { WantOutcome val2 = GuestReplicas.WantSpecies(text, front: true); if ((int)val2 != 0) { if ((int)val2 == 1) { Plugin.Log.LogMessage((object)("[MIRROR] master wants '" + text + "' warmed — already queued (not promoted: one is drained per attempt anyway).")); } else { Plugin.Log.LogMessage((object)("[MIRROR] master wants '" + text + "' warmed — DECLINED: this machine has proven it cannot harvest that species this session (dead end). The host's row already shows it, so the ask will not be repeated.")); } } else { Plugin.Log.LogMessage((object)("[MIRROR] master wants '" + text + "' warmed — queued at head.")); } } }, (HandlerRole)4); ch.OnPeerSceneReady += delegate(PeerInfo info) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) FlushSpawnsTo(info.Actor); }; ch.OnPeerSceneReady += delegate(PeerInfo info) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) if (!PhotonNetwork.isNonMasterClientInRoom) { Spawner.RaisePeerSceneReady(info.Actor); } }; ch.OnPeerReady += delegate(PeerInfo info) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) if (!ch.IsPeerSceneReady(info.Actor)) { Plugin.Log.LogMessage((object)($"[SKNET] actor {info.Actor} is sk-ready but still LOADING — spawn " + "flush deferred to its scene-ready hello (N-3).")); } }; Net.OnRoomChanged += delegate { _flushedTo.Clear(); _forgiven.Clear(); GuestReplicas.OnRoomChanged(); if (ShouldAnnounce()) { int num = 0; List list = PresentActors(); foreach (SpawnHandle item in EnemySpawner.Snapshot(null)) { if (item.State == SpawnState.Alive) { string text = EncodeSpawnFor(item); if (text != null && _store.Announce(item.Uid, text, "")) { num++; for (int i = 0; i < list.Count; i++) { RecordFlushed(list[i], item.Uid); } } } } if (num > 0) { Plugin.Log.LogMessage((object)($"[SKNET] room changed — re-announced {num} live spawn(s) " + "into the store book (registry truth survives the book reset).")); } } }; Net.OnSceneReady += OnSceneReadyResync; return ch; } internal static void Init() { NetChannel channel = Channel; } private static string HeartbeatFragment() { return $"spawns={EnemySpawner.TrackedCount} replicas={GuestReplicas.Count} queue={GuestReplicas.QueueCount}"; } internal static bool SendToMaster(string verb, string payload) { return Channel.SendToMaster(verb, payload ?? "", ""); } internal static bool SendToOthers(string verb, string payload) { return Channel.SendToOthers(verb, payload ?? "", ""); } internal static bool SendToActor(int actor, string verb, string payload) { return Channel.SendToActor(actor, verb, payload ?? "", ""); } internal static bool SendToAllLoopback(string verb, string payload) { return Channel.SendToAllLoopback(verb, payload ?? "", ""); } internal static void CountDrop(string verb, string reason) { Channel.CountDrop(verb ?? "?", reason ?? "?"); } internal static int PeersWithoutHelloCount(out string names) { names = ""; if (!PhotonNetwork.inRoom) { return 0; } PhotonPlayer[] array = null; try { array = PhotonNetwork.otherPlayers; } catch { } if (array == null) { return 0; } NetChannel channel = Channel; int num = 0; StringBuilder stringBuilder = new StringBuilder(); PhotonPlayer[] array2 = array; foreach (PhotonPlayer val in array2) { if (val != null && !channel.IsPeerReady(val.ID)) { num++; if (stringBuilder.Length > 0) { stringBuilder.Append(", "); } stringBuilder.Append("actor ").Append(val.ID); } } names = stringBuilder.ToString(); return num; } private static void RecordFlushed(int actorId, string uid) { if (string.IsNullOrEmpty(uid)) { return; } if (!_flushedTo.TryGetValue(actorId, out var value)) { value = (_flushedTo[actorId] = new List()); } if (!value.Contains(uid)) { value.Add(uid); if (value.Count > 128) { value.RemoveAt(0); } } } private static List PresentActors() { List list = new List(); try { PhotonPlayer[] otherPlayers = PhotonNetwork.otherPlayers; if (otherPlayers != null) { PhotonPlayer[] array = otherPlayers; foreach (PhotonPlayer val in array) { if (val != null) { list.Add(val.ID); } } } } catch { } return list; } private static bool WasFlushedTo(int actorId, string uid) { if (_flushedTo.TryGetValue(actorId, out var value)) { return value.Contains(uid); } return false; } private static double ForgivenLifetimeSeconds() { return (double)((Plugin.MirrorQueueTimeoutSeconds != null) ? Plugin.MirrorQueueTimeoutSeconds.Value : 20f) * 3.0; } internal static void OnPeerRowGained(int actor, string[] warm) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) if (_forgiven.Count == 0 || !IsRoomHostRaw()) { return; } List list = _forgiven.MatchWarm(actor, warm); if (list.Count == 0) { return; } FlushSpawnsTo(actor); List list2 = new List(); for (int i = 0; i < list.Count; i++) { if (!list2.Contains(list[i].Species)) { list2.Add(list[i].Species); } } Plugin.Log.LogMessage((object)($"[SKNET] re-flushed {list.Count} forgiven spawn(s) to actor {actor} — " + "its row now shows '" + string.Join(", ", list2.ToArray()) + "' warm")); } internal static void OnPeerRowLost(int actor) { _forgiven.ForgetActor(actor); } internal static void TickForgiven() { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) if (_forgiven.Count == 0) { return; } float unscaledTime = Time.unscaledTime; if (unscaledTime < _nextForgivenSweepAt) { return; } _nextForgivenSweepAt = unscaledTime + 2f; List list = _forgiven.Snapshot(); for (int i = 0; i < list.Count; i++) { SpawnHandle spawnHandle = EnemySpawner.FindByUid(list[i].Uid); if (spawnHandle == null || spawnHandle.State != SpawnState.Alive) { _forgiven.ForgetUid(list[i].Uid); } } List list2 = _forgiven.TakeExpired((double)unscaledTime, ForgivenLifetimeSeconds()); for (int j = 0; j < list2.Count; j++) { SpawnHandle spawnHandle2 = EnemySpawner.FindByUid(list2[j].Uid); Plugin.Log.LogWarning((object)("[SKNET] forgiven cold-unsafe for uid " + list2[j].Uid + " never resolved — despawning")); if (spawnHandle2 != null && spawnHandle2.State == SpawnState.Alive) { EnemySpawner.Despawn(spawnHandle2, kill: false); } } } internal static void BroadcastSpawn(SpawnHandle handle) { if (!ShouldAnnounce()) { return; } string text = EncodeSpawnFor(handle); if (text != null && _store.Announce(handle.Uid, text, "")) { List list = PresentActors(); for (int i = 0; i < list.Count; i++) { RecordFlushed(list[i], handle.Uid); } } } internal static void FlushSpawnsTo(int actorId) { if (ShouldSpeak()) { int num = _store.FlushTo(actorId, (Action)delegate(string uid) { RecordFlushed(actorId, uid); }); if (num > 0) { Plugin.Log.LogMessage((object)$"[SKNET] flushed {num} Alive spawn(s) to actor {actorId} (peer-ready / resync — guest dedup handles replays)."); } } } private static void OnResyncRequest(int actor) { if (PhotonNetwork.isMasterClient) { Plugin.Log.LogMessage((object)$"[SKNET] sk.resync from actor {actor} — re-flushing Alive spawns."); FlushSpawnsTo(actor); } } private static void OnSceneReadyResync(string scene) { if (Net.IsGuestInRoom && Net.Attached && Channel.SendToMaster("sk.resync", "", "")) { Plugin.Log.LogMessage((object)("[SKNET] scene-ready resync — asked the master to re-flush its Alive spawns (sk.resync, scene '" + scene + "').")); } } internal static void RequestResync() { bool flag = IsRoomHostRaw(); if (Net.InRoom && !flag) { if (Channel.SendToMaster("sk.resync", "", "")) { Plugin.Log.LogMessage((object)"[SKNET] skresync — asked the master to re-flush its Alive spawns (sk.resync)."); } else { Plugin.Log.LogWarning((object)"[SKNET] skresync — sk.resync send dropped (not attached / not in room). See netdump."); } } else if (Net.InRoom && flag) { int num = 0; int num2 = 0; NetChannel channel = Channel; foreach (int item in PresentActors()) { if (!channel.IsPeerReady(item) || !channel.IsPeerSceneReady(item)) { num2++; Plugin.Log.LogMessage((object)($"[SKNET] skresync — actor {item} skipped (ready={channel.IsPeerReady(item)}, " + $"sceneReady={channel.IsPeerSceneReady(item)}); its scene-ready hello will re-ask (N-3).")); } else { FlushSpawnsTo(item); num++; } } Plugin.Log.LogMessage((object)($"[SKNET] skresync (master) — re-flushed Alive spawns to {num} peer(s)" + ((num2 > 0) ? $", {num2} skipped as not-ready." : "."))); } else { Plugin.Log.LogMessage((object)"[SKNET] skresync — not in a co-op room; nothing to resync."); } } private static bool ShouldSpeak() { if (!ShouldAnnounce()) { return false; } try { PhotonPlayer[] otherPlayers = PhotonNetwork.otherPlayers; return ((otherPlayers != null && otherPlayers.Length != 0) ? 1 : 0) > (false ? 1 : 0); } catch { return false; } } private static bool ShouldAnnounce() { if (!IsRoomHostRaw() || !PhotonNetwork.inRoom) { return false; } if (Plugin.EnableCoopSpawns != null) { return Plugin.EnableCoopSpawns.Value; } return false; } internal static bool IsRoomHostRaw() { try { if (!PhotonNetwork.inRoom) { return true; } PhotonPlayer player = PhotonNetwork.player; return player != null && player.IsMasterClient; } catch { return true; } } private static string EncodeSpawnFor(SpawnHandle h) { //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: 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_0100: 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) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_0178: Unknown result type (might be due to invalid IL or missing references) Character character = h.Character; if ((Object)(object)character == (Object)null) { return null; } if (h.ViewId < 0) { CountDrop("sk.spawn", "no-viewid"); Plugin.Log.LogWarning((object)("[SKNET] refusing to broadcast spawn uid=" + h.Uid + " species='" + h.SpeciesKey + "' — it has NO minted viewID (viewId=-1; the master mint found no PhotonView). Guests can't mirror a view-less spawn; it stays master-local.")); return null; } Vector3 position = ((Component)character).transform.position; int itemId = 0; int itemId2 = 0; string uid = ""; string uid2 = ""; if (Plugin.ShipWeaponIdentity != null && Plugin.ShipWeaponIdentity.Value) { HumanoidWeapon.HandIdentity(character, (EquipmentSlotIDs)5, out itemId, out uid); HumanoidWeapon.HandIdentity(character, (EquipmentSlotIDs)6, out itemId2, out uid2); } SpawnMsg val = new SpawnMsg { Proto = 1, SpeciesKey = h.SpeciesKey, Uid = h.Uid, ViewId = h.ViewId }; Scene activeScene = SceneManager.GetActiveScene(); val.Scene = ((Scene)(ref activeScene)).name; val.X = position.x; val.Y = position.y; val.Z = position.z; val.YawDeg = ((Component)character).transform.eulerAngles.y; val.Faction = h.CoopFaction; val.StripQuestEvents = h.CoopStripQuestEvents; val.RightHandItemId = itemId; val.RightHandItemUid = uid; val.LeftHandItemId = itemId2; val.LeftHandItemUid = uid2; return SpawnNetProtocol.EncodeSpawn(val); } internal static void SendGone(string uid, GoneKind kind) { //IL_000b: 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_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Invalid comparison between Unknown and I4 if (string.IsNullOrEmpty(uid)) { return; } string text = SpawnNetProtocol.EncodeGone(new GoneMsg { Uid = uid, Kind = kind }); RecordRow val = default(RecordRow); if ((int)kind == 2) { if (ShouldSpeak()) { SendToOthers("sk.corpse", text); } } else if (_store != null && _store.TryGet(uid, ref val)) { _store.Release(uid, text); } } private static void OnSpawnRecordSet(string key, string payload, RecordMeta meta) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) if (PhotonNetwork.isNonMasterClientInRoom) { string expectedUid = default(string); int num = default(int); RecordKey.TryParse(key, ref expectedUid, ref num); GuestReplicas.OnSpawnMessage(payload, meta.SenderActor, expectedUid); } } private static void OnSpawnRecordCleared(string key, string reason, RecordMeta meta) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) if (PhotonNetwork.isNonMasterClientInRoom) { string uid = default(string); int num = default(int); RecordKey.TryParse(key, ref uid, ref num); GuestReplicas.OnRecordCleared(uid, reason, meta.SenderActor); } } private static void OnAck(string payload, int actor) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_006a: 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) AckMsg val = default(AckMsg); if (!SpawnNetProtocol.TryDecodeAck(payload, ref val)) { CountDrop("sk.ack", "unparseable"); return; } Report(val.Uid, $"ack actor={actor} src={val.Source} tookMs={val.TookMs}"); Plugin.Log.LogMessage((object)$"[SKNET] mirror ACK uid={val.Uid} actor={actor} src={val.Source} tookMs={val.TookMs}."); } private static void OnFail(string payload, int actor) { //IL_006c: 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_008d: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: 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_004b: 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) //IL_01b9: 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_014a: Unknown result type (might be due to invalid IL or missing references) //IL_021e: Unknown result type (might be due to invalid IL or missing references) FailMsg val = default(FailMsg); if (!SpawnNetProtocol.TryDecodeFail(payload, ref val)) { CountDrop("sk.fail", "unparseable"); return; } if (actor != -1 && !WasFlushedTo(actor, val.Uid)) { CountDrop("sk.fail", "not-flushed"); Plugin.Log.LogWarning((object)($"[SKNET] refuse sk.fail uid={val.Uid} reason={val.Reason} from actor {actor} — " + "that uid was never flushed to this actor, so it has nothing to report on (forged, stale across a room change, or version skew). Dropped; the spawn stands.")); return; } Report(val.Uid, $"FAIL actor={actor} reason={val.Reason}"); if (MirrorFailPolicy.IsBenign(val.Reason)) { Plugin.Log.LogMessage((object)($"[SKNET] mirror deferred uid={val.Uid} actor={actor} reason={val.Reason} — " + "the guest was not in-world yet; spawn KEPT (benign, N-3). Its scene-ready resync re-asks.")); return; } SpawnHandle spawnHandle = EnemySpawner.FindByUid(val.Uid); if (string.Equals(val.Reason, "cold-unsafe", StringComparison.Ordinal)) { bool flag = spawnHandle?.RoomGateOk ?? false; int num = spawnHandle?.NoteColdUnsafeFrom(actor) ?? 0; if (!MirrorFailPolicy.ShouldDespawnColdUnsafe(flag, num)) { Forgiven.Add(actor, val.Uid, spawnHandle.SpeciesKey, (double)Time.unscaledTime); CountDrop("sk.spawn", "cold-unsafe-forgiven"); Plugin.Log.LogWarning((object)($"[SKNET] cold-unsafe DESPITE room gate for uid {val.Uid} (actor {actor}) — " + "requesting warm, keeping the spawn. That peer published this species as warm and has since evicted it; its next publish will tell the truth (skwarmdump). The uid is owed back to that actor and re-flushes when its row shows the species warm.")); Spawner.RequestRoomWarm(new string[1] { spawnHandle.SpeciesKey }); return; } } bool flag2 = Plugin.DespawnOnMirrorFailure != null && Plugin.DespawnOnMirrorFailure.Value && spawnHandle != null && spawnHandle.State == SpawnState.Alive; Plugin.Log.LogWarning((object)($"[SKNET] mirror FAILED uid={val.Uid} actor={actor} reason={val.Reason} — " + (flag2 ? "despawning it ([Coop] DespawnOnMirrorFailure=true): a spawn one guest can't see is the old ghost-combat hazard." : ((spawnHandle == null) ? "handle already gone; nothing to do." : "keeping it ([Coop] DespawnOnMirrorFailure=false) — that guest gets the pre-Phase-3 ghost behavior.")))); if (flag2) { Character val2 = Lifecycle.FirstLocalCharacterOrNull(); if ((Object)(object)val2 != (Object)null) { Notify.Player(val2, "Spawn removed: a guest could not mirror '" + spawnHandle.SpeciesKey + "' (" + val.Reason + ")."); } EnemySpawner.Despawn(spawnHandle, kill: false); } } private static void Report(string uid, string line) { if (!string.IsNullOrEmpty(uid)) { if (!_mirrorReports.TryGetValue(uid, out var value)) { value = (_mirrorReports[uid] = new MirrorReport()); _mirrorReportOrder.Add(uid); } value.Lines.Add($"{Time.unscaledTime:F0}s {line}"); if (value.Lines.Count > 32) { value.Lines.RemoveAt(0); } while (_mirrorReportOrder.Count > 64) { _mirrorReports.Remove(_mirrorReportOrder[0]); _mirrorReportOrder.RemoveAt(0); } } } internal static void InjectLocal(string verb, string payload) { string text = ((payload != null && payload.Length > 60) ? (payload.Substring(0, 60) + "…") : (payload ?? "")); Plugin.Log.LogMessage((object)("[SKNET] [inject] " + verb + ": " + text)); try { switch (verb) { case "sk.spawn": GuestReplicas.OnSpawnMessage(payload, -1); break; case "sk.gone": GuestReplicas.OnGoneMessage(payload, -1, "sk.gone"); break; case "sk.corpse": GuestReplicas.OnGoneMessage(payload, -1, "sk.corpse"); break; case "sk.ack": OnAck(payload, -1); break; case "sk.fail": OnFail(payload, -1); break; case "sk.resync": OnResyncRequest(-1); break; case "sk.test": LastTestPayload = payload; break; default: Plugin.Log.LogWarning((object)("[SKNET] [inject] unknown verb '" + verb + "'.")); break; } } catch (Exception arg) { Plugin.Log.LogWarning((object)$"[SKNET] [inject] handler '{verb}' threw: {arg}"); } } internal static string Dump() { //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_015c: Unknown result type (might be due to invalid IL or missing references) //IL_0164: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_0172: Unknown result type (might be due to invalid IL or missing references) //IL_018e: Unknown result type (might be due to invalid IL or missing references) StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine($"[SKNET] coop={Plugin.EnableCoopSpawns != null && Plugin.EnableCoopSpawns.Value} " + $"inRoom={PhotonNetwork.inRoom} isMaster={PhotonNetwork.isMasterClient} attached={Net.Attached}"); if (_mirrorReports.Count > 0) { stringBuilder.AppendLine($"[SKNET] mirror reports ({_mirrorReports.Count} uid(s)):"); foreach (KeyValuePair mirrorReport in _mirrorReports) { stringBuilder.AppendLine("[SKNET] " + mirrorReport.Key + ": " + string.Join(" | ", mirrorReport.Value.Lines.ToArray())); } } else { stringBuilder.AppendLine("[SKNET] no mirror ack/fail reports yet."); } List list = _forgiven.Snapshot(); if (list.Count > 0) { stringBuilder.AppendLine($"[SKNET] forgiven cold-unsafe ({list.Count}; despawned if unresolved after " + $"{ForgivenLifetimeSeconds():F0}s):"); foreach (ForgivenEntry item in list) { stringBuilder.AppendLine($"[SKNET] uid={item.Uid} species='{item.Species}' owed to actor {item.Actor} " + $"(forgiven {(double)Time.unscaledTime - item.At:F0}s ago)"); } } stringBuilder.AppendLine(WarmMirror.Dump()); stringBuilder.Append(Net.Dump()); return stringBuilder.ToString().TrimEnd(Array.Empty()); } } public sealed class SpawnOptions { public float? Distance; public Vector3? Position; public Quaternion? Rotation; public Factions? Faction; public float? LifetimeSeconds; public CorpsePolicy? Corpse; public float? CorpseLingerSeconds; public bool StripQuestEvents = true; public bool IgnoreRoomWarm; public string OwnerTag; public Action OnBeforeActivate; public string ConsumerData; } internal static class SpawnTemplates { private static readonly Dictionary>> _pending = new Dictionary>>(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary _resolvedDonor = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly HashSet _refusedSpecies = new HashSet(StringComparer.OrdinalIgnoreCase); private static TemplateKind Kind => TemplateStore.Spawnables; public static string ResolvedDonorName(string speciesKey) { if (string.IsNullOrEmpty(speciesKey) || !_resolvedDonor.TryGetValue(speciesKey.Trim(), out var value)) { return null; } return value; } public static IEnumerator AcquireScene(string sceneName, Action onDone) { if (EnemySpawner.RefusedNonMasterHarvest("[PREP]")) { onDone.Invoke(0, 0); yield break; } List list = DonorTable.KeysForScene(DonorHarvest.DonorScenes, sceneName); List pending = new List(); GameObject val = default(GameObject); foreach (string item in list) { if (!Kind.TryGet(item, ref val) || (Object)(object)val == (Object)null) { pending.Add(item); } } if (pending.Count == 0) { Plugin.Log.LogMessage((object)$"[PREP] done '{sceneName}': nothing to build ({list.Count} species listed, all cached or none)."); onDone.Invoke(0, 0); yield break; } int built = 0; List missing = new List(); object result = null; yield return DonorHarvest.HarvestScene(sceneName, $"batch={pending.Count} species", (Func)delegate(Scene donor) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) foreach (string item2 in pending) { Character val2 = DonorHarvest.FindLiveInScene(donor, item2); if ((Object)(object)val2 == (Object)null) { missing.Add(item2); } else { GameObject val3 = null; try { object obj = BuildTemplate(val2, item2); val3 = (GameObject)((obj is GameObject) ? obj : null); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[PREP] '" + item2 + "' failed to build (" + ex.GetType().Name + ": " + ex.Message + ") — continuing with the rest of the batch.")); } if ((Object)(object)val3 != (Object)null) { Insert(item2, val3); built++; } else { missing.Add(item2); } } } return built; }, (Action)delegate(object r) { result = r; }); if (result == null && built == 0 && missing.Count == 0) { missing.AddRange(pending); } Plugin.Log.LogMessage((object)($"[PREP] done '{sceneName}': built {built}/{pending.Count}" + ((missing.Count > 0) ? (", missing: " + string.Join(", ", missing)) : "") + ".")); onDone.Invoke(built, missing.Count); } public static bool IsCached(string speciesKey) { GameObject val = default(GameObject); if (!string.IsNullOrEmpty(speciesKey) && Kind.TryGet(speciesKey, ref val)) { return (Object)(object)val != (Object)null; } return false; } public static IEnumerator Acquire(string speciesKey, Action onTemplate, bool allowNonMaster = false, int maxDonorCandidates = 0) { speciesKey = speciesKey?.Trim() ?? ""; if (!allowNonMaster && EnemySpawner.RefusedNonMasterHarvest("[TEMPLATE]")) { onTemplate(null); yield break; } if (allowNonMaster && PhotonNetwork.isNonMasterClientInRoom && !IsCached(speciesKey)) { Plugin.Log.LogMessage((object)("[TEMPLATE] guest mirror harvest for '" + speciesKey + "' (master-instructed — the one sanctioned non-master donor load).")); } GameObject val = default(GameObject); if (Kind.TryGet(speciesKey, ref val)) { if ((Object)(object)val != (Object)null) { Kind.Touch(speciesKey); Plugin.Log.LogMessage((object)("[TEMPLATE] cache hit for '" + speciesKey + "' — minting from the resident template (no donor load).")); onTemplate(val); yield break; } RemoveKey(speciesKey); Plugin.Log.LogWarning((object)("[TEMPLATE] cached template for '" + speciesKey + "' was destroyed — re-harvesting.")); } if (_refusedSpecies.Contains(speciesKey)) { Plugin.Log.LogMessage((object)("[TEMPLATE] '" + speciesKey + "' was refused by the AI gate earlier this session (BUG-PREBUILTADOPT) — skipping the re-harvest; 'spawnclearcache' resets the refusal.")); onTemplate(null); yield break; } if (_pending.TryGetValue(speciesKey, out var value)) { value.Add(onTemplate); yield break; } _pending[speciesKey] = new List> { onTemplate }; GameObject resolved = null; try { GameObject val2 = AdoptFromExpedition(speciesKey); if ((Object)(object)val2 != (Object)null) { Insert(speciesKey, val2); Plugin.Log.LogMessage((object)("[TEMPLATE] adopted '" + speciesKey + "' from the expedition body-template cache — no donor load needed.")); resolved = val2; yield break; } List list = default(List); string text = default(string); if (!DonorHarvest.TryGetDonorScenes(speciesKey, ref list, ref text)) { Plugin.Log.LogWarning((object)("[TEMPLATE] no DonorScenes entry matches '" + speciesKey + "' (and the expedition cache has no template for it) — 'spawnlist' shows the table; an 'expedition' can supply region-only species.")); yield break; } object result = null; yield return DonorHarvest.HarvestChain(list, text, (Func)((Character src) => BuildTemplate(src, speciesKey)), (Action)delegate(object r) { result = r; }, maxDonorCandidates); object obj = result; GameObject val3 = (GameObject)((obj is GameObject) ? obj : null); if ((Object)(object)val3 != (Object)null) { Insert(speciesKey, val3); } resolved = val3; } finally { Deliver(speciesKey, resolved); } } private static void Deliver(string speciesKey, GameObject template) { if (!_pending.TryGetValue(speciesKey, out var value)) { return; } _pending.Remove(speciesKey); if (value.Count > 1) { Plugin.Log.LogMessage((object)$"[TEMPLATE] '{speciesKey}': one harvest served {value.Count} concurrent acquires."); } foreach (Action item in value) { try { item?.Invoke(template); } catch (Exception arg) { Plugin.Log.LogError((object)$"[TEMPLATE] acquire callback for '{speciesKey}' threw: {arg}"); } } } private static void RemoveKey(string key, bool destroy = false) { GameObject val = default(GameObject); if (destroy && Kind.TryGet(key, ref val) && (Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } Kind.Remove(key); } private static void Insert(string key, GameObject template) { _refusedSpecies.Remove(key?.Trim() ?? ""); Kind.Put(key, template); int num = ((Plugin.MaxCachedTemplates != null) ? Plugin.MaxCachedTemplates.Value : 0); if (num > 0) { foreach (string item in SpawnCap.PickEvict(Kind.LruView, Kind.RawCount, num, (ICollection)_pending.Keys)) { Plugin.Log.LogMessage((object)$"[TEMPLATE] LRU cap ({num}): evicting '{item}' (next spawn of it re-harvests)."); RemoveKey(item, destroy: true); } return; } if (SpawnCap.ShouldWarnCacheSize(Kind.RawCount, 8)) { Plugin.Log.LogWarning((object)($"[TEMPLATE] cache now holds {Kind.RawCount} resident creature template(s) — " + "each pins its mesh/texture/audio in memory. 'spawnclearcache' frees them (safe when nothing is mid-spawn).")); } } private static object BuildTemplate(Character src, string speciesKey) { GameObject val = Object.Instantiate(((Component)src).gameObject, Kind.Holder().transform); try { Normalize(val, speciesKey, "donor '" + src.Name + "'"); RecordDonorName(speciesKey, DonorHarvest.IdentityFor(speciesKey, src)); string text = RagdollRig.RestoreJoints(((Component)src).gameObject, val, (Action)delegate(string w) { Plugin.Log.LogWarning((object)("[TEMPLATE] ragdoll ('" + speciesKey + "'): " + w)); }); if (text != null) { Plugin.Log.LogMessage((object)("[TEMPLATE] ragdoll ('" + speciesKey + "'): " + text + ".")); } if (!RigCheck(((Component)src).gameObject, val, speciesKey)) { Object.Destroy((Object)(object)val); return null; } return val; } catch (Exception arg) { Plugin.Log.LogError((object)$"[TEMPLATE] build of '{speciesKey}' threw — destroying orphan clone under the holder: {arg}"); if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } throw; } } private static bool RigCheck(GameObject donor, GameObject clone, string speciesKey) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Invalid comparison between Unknown and I4 if ((int)SkeletonRig.RunGate(donor, clone, speciesKey, ModLog.op_Implicit(Plugin.Log), "spawns of this template") != 3) { return true; } Plugin.Log.LogWarning((object)("[TEMPLATE] rig ('" + speciesKey + "'): UNREPAIRABLE after repair pass — rejecting this donor; the harvest chain advances to the next scene.")); return false; } private static void RecordDonorName(string speciesKey, string donorName) { if (!string.IsNullOrEmpty(speciesKey) && !string.IsNullOrEmpty(donorName)) { string text = speciesKey.Trim(); bool flag = !_resolvedDonor.ContainsKey(text); _resolvedDonor[text] = donorName; if (flag && SpawnMenuLabels.IsDonorMismatch(text, donorName)) { Plugin.Log.LogWarning((object)("[SPAWN] label: species key '" + text + "' resolves to donor creature '" + donorName + "' (the menu row / spawn verb name is a NAME-MATCH substring, not the exact creature — a spawn of '" + text + "' produces a '" + donorName + "'). The menu row now shows this.")); } } } private static GameObject AdoptFromExpedition(string speciesKey) { BodyTemplate val = default(BodyTemplate); try { if (!BodyTemplateCache.TryResolveExact(speciesKey, ref val) || (Object)(object)val.Dormant == (Object)null) { return null; } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[TEMPLATE] expedition-cache lookup for '" + speciesKey + "' threw: " + ex.Message)); return null; } if (!IsAdoptable(val.Dormant)) { _refusedSpecies.Add(speciesKey?.Trim() ?? ""); Plugin.Log.LogWarning((object)("[TEMPLATE] " + TemplateAiGate.Reason(speciesKey, "expedition cache") + " — refusing adoption; falling through to the donor harvest chain (the prebuilt body stays valid for the PET path).")); return null; } GameObject val2 = Object.Instantiate(val.Dormant, Kind.Holder().transform); try { Normalize(val2, speciesKey, "expedition cache (species '" + val.SpeciesId + "')"); return val2; } catch (Exception arg) { Plugin.Log.LogError((object)$"[TEMPLATE] adoption of '{speciesKey}' from the expedition cache threw — destroying orphan clone: {arg}"); if ((Object)(object)val2 != (Object)null) { Object.Destroy((Object)(object)val2); } return null; } } internal static bool IsAdoptable(GameObject dormant) { if ((Object)(object)dormant == (Object)null) { return false; } try { CharacterAI component = dormant.GetComponent(); bool flag = (Object)(object)component != (Object)null && (Object)(object)component.AIStatesPrefab != (Object)null; int num = dormant.GetComponentsInChildren(true).Length; return !TemplateAiGate.RefusesAdoption((Object)(object)component != (Object)null, flag, PhotonNetwork.isNonMasterClientInRoom, num); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[TEMPLATE] AI-graph admission test on '" + ((Object)dormant).name + "' threw (" + ex.GetType().Name + ": " + ex.Message + ") — treating as not adoptable.")); return false; } } private static void Normalize(GameObject go, string speciesKey, string sourceLabel) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) ((Object)go).name = "SK_Template_" + speciesKey; go.transform.localPosition = Vector3.zero; PhotonView component = go.GetComponent(); int num = (((Object)(object)component != (Object)null) ? component.viewID : (-1)); if ((Object)(object)component != (Object)null) { component.viewID = 0; } int num2 = 0; AISquadMember[] componentsInChildren = go.GetComponentsInChildren(true); foreach (AISquadMember val in componentsInChildren) { if ((Object)(object)val != (Object)null && (Object)(object)val.AISquad != (Object)null) { val.AISquad = null; num2++; } } int num3 = 0; int num4 = 0; AIRoot[] componentsInChildren2 = go.GetComponentsInChildren(true); foreach (AIRoot val2 in componentsInChildren2) { if (!((Object)(object)val2 == (Object)null)) { num3++; if (!((Component)val2).gameObject.activeSelf) { ((Component)val2).gameObject.SetActive(true); num4++; } } } int num5 = 0; int num6 = 0; AIState[] componentsInChildren3 = go.GetComponentsInChildren(true); foreach (AIState val3 in componentsInChildren3) { if (!((Object)(object)val3 == (Object)null)) { num5++; if (!((Component)val3).gameObject.activeSelf) { ((Component)val3).gameObject.SetActive(true); num6++; } } } Character component2 = go.GetComponent(); CharacterAI component3 = go.GetComponent(); bool flag = false; try { flag = (Object)(object)component3 != (Object)null && (Object)(object)component3.AIStatesPrefab != (Object)null; } catch { } int num7 = 0; int num8 = 0; try { num7 = go.GetComponents().Length; } catch { } try { num8 = go.GetComponents().Length; } catch { } int num9 = go.GetComponentsInChildren(true).Length; int num10 = go.GetComponentsInChildren(true).Length; int num11 = go.GetComponentsInChildren(true).Length; string text = "?"; string text2 = "?"; try { text = ((object)Unsafe.As(ref component2.Faction)/*cast due to .constrained prefix*/).ToString(); } catch { } try { text2 = component2.Lifetime.ToString("0.#"); } catch { } string text3 = "-"; QuestEventReference val4 = null; try { FieldInfo fieldInfo = AccessTools.Field(typeof(CharacterAI), "m_aiActiveOnQuestEvent"); val4 = (QuestEventReference)(((Object)(object)component3 != (Object)null && fieldInfo != null) ? /*isinst with value type is only supported in some contexts*/: null); if (val4 != null && !string.IsNullOrEmpty(val4.EventUID)) { text3 = val4.EventUID; } } catch { text3 = "?"; } int num12 = 0; int num13 = 0; try { num12 = go.GetComponentsInChildren(true).Length; } catch { } try { num13 = go.GetComponentsInChildren(true).Length + go.GetComponentsInChildren(true).Length; } catch { } string arg = "?"; try { arg = (((Object)(object)component2 != (Object)null) ? component2.Undying.ToString() : "?"); } catch { } Plugin.Log.LogMessage((object)("[TEMPLATE] built '" + speciesKey + "' from " + sourceLabel + ": " + string.Format("donorViewID={0} (zeroed) | charAI={1} ncc={2} charAIDisable={3} | ", num, ((Object)(object)component3 != (Object)null) ? "SET" : "MISSING", num7, num8) + "AIStatesPrefab=" + (flag ? "SET" : "null") + " " + $"AIRoot children={num3} (forced active {num4}) AIStates={num5} (forced active {num6}) | squad refs nulled={num2} | " + $"LootableOnDeath={num9} Dropable={num10} QuestEventOnDeath={num11} | " + "faction=" + text + " Lifetime=" + text2)); Plugin.Log.LogMessage((object)($"[TEMPLATE] census '{speciesKey}': aiQuestGate={text3} invulnQE={num12} " + $"undying={arg} achOnDeath={num13}")); if (Plugin.ClearAiQuestGate != null && Plugin.ClearAiQuestGate.Value && text3 != "-" && text3 != "?") { try { FieldInfo fieldInfo2 = AccessTools.Field(typeof(QuestEventReference), "m_eventUID"); if (val4 == null || fieldInfo2 == null) { throw new MissingFieldException("QuestEventReference.m_eventUID not found"); } fieldInfo2.SetValue(val4, ""); Plugin.Log.LogMessage((object)("[TEMPLATE] cleared AI quest gate on '" + speciesKey + "' (was EventUID='" + text3 + "') — spawns of this species keep their AI root active instead of waiting on a quest event that never fires.")); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[TEMPLATE] AI quest-gate clear threw on '" + speciesKey + "' (" + ex.GetType().Name + ": " + ex.Message + ") — template keeps the donor gate; expect a dormant spawn if the event is unset.")); } } if ((Object)(object)component3 == (Object)null && num3 >= 1 && PhotonNetwork.isNonMasterClientInRoom) { Plugin.Log.LogWarning((object)("[TEMPLATE] '" + speciesKey + "' was built from an ALREADY-CONVERTED donor (charAI=MISSING on a guest) — the [HARVEST-GUARD] NetworkInit suppression did not cover this harvest; guest mints will rebuild the drive by hand (CORRUPT-TEMPLATE path).")); } if (TemplateAiGate.RefusesAdoption((Object)(object)component3 != (Object)null, flag, PhotonNetwork.isNonMasterClientInRoom, num3)) { _refusedSpecies.Add(speciesKey?.Trim() ?? ""); throw new InvalidOperationException("[TEMPLATE] " + TemplateAiGate.Reason(speciesKey, sourceLabel) + " — refusing to cache this template (risk #1's worst case; see docs/spawnkit-plan.md fallback ladder)."); } } public static int Clear() { _refusedSpecies.Clear(); return Kind.ClearAll(); } public static string Dump() { if (Kind.RawCount == 0) { return "[TEMPLATE] prewarm cache empty (this is SpawnKit's own; the expedition body-template cache is separate)."; } string arg = (SpawnCap.IsCacheLarge(Kind.RawCount, 8) ? " — 'spawnclearcache' frees the pinned mesh/texture/audio assets" : ""); StringBuilder stringBuilder = new StringBuilder($"[TEMPLATE] {Kind.RawCount} cached template(s){arg}:"); foreach (KeyValuePair entry in Kind.Entries) { stringBuilder.Append("\n '" + entry.Key + "' -> " + (((Object)(object)entry.Value != (Object)null) ? ((Object)entry.Value).name : "")); } return stringBuilder.ToString(); } internal static void NormalizeRegistered(GameObject go, string speciesKey) { Normalize(go, speciesKey, "third-party registration (TemplateStore.RegisterTemplate)"); } } internal static class VisualPass { internal static Census Measure(GameObject go) { //IL_0002: 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_01f6: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00de: 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_010e: Unknown result type (might be due to invalid IL or missing references) //IL_0113: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Expected O, but got Unknown //IL_0158: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_016a: Unknown result type (might be due to invalid IL or missing references) //IL_0178: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) Census result = default(Census); if ((Object)(object)go == (Object)null) { return result; } try { SkinnedMeshRenderer[] componentsInChildren = go.GetComponentsInChildren(true); result.Renderers = componentsInChildren.Length; result.RenderReady = BodyFactory.CountRenderReady(go); SkinnedMeshRenderer[] array = componentsInChildren; foreach (SkinnedMeshRenderer val in array) { if ((Object)(object)val != (Object)null && ((Renderer)val).enabled && ((Component)val).gameObject.activeInHierarchy && !((Renderer)val).forceRenderingOff && (Object)(object)((Renderer)val).sharedMaterial != (Object)null && (Object)(object)val.sharedMesh != (Object)null) { result.SkinnedReady++; } } SkinnedMeshRenderer val2 = LargestSmr(go); if ((Object)(object)val2 != (Object)null) { Transform[] bones = val2.bones; result.TotalBones = bones.Length; Vector3 val3 = (((Object)(object)val2.rootBone != (Object)null) ? val2.rootBone.position : ((Component)val2).transform.position); Transform[] array2 = bones; foreach (Transform val4 in array2) { if ((Object)(object)val4 != (Object)null) { Vector3 val5 = val4.position - val3; if (((Vector3)(ref val5)).sqrMagnitude < 0.0001f) { result.StackedBones++; } } } Mesh val6 = null; try { val6 = new Mesh(); val2.BakeMesh(val6); Bounds bounds = val6.bounds; Vector3 size = ((Bounds)(ref bounds)).size; result.BakedX = size.x; result.BakedY = size.y; result.BakedZ = size.z; } finally { if ((Object)(object)val6 != (Object)null) { Object.DestroyImmediate((Object)(object)val6); } } } Animator componentInChildren = go.GetComponentInChildren(true); result.AnimatorPresent = (Object)(object)componentInChildren != (Object)null; result.AnimatorInitialized = (Object)(object)componentInChildren != (Object)null && componentInChildren.isInitialized; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SPAWN] visual measure threw: " + ex.Message)); } return result; } internal static bool Run(Character ch, string speciesKey) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0215: Unknown result type (might be due to invalid IL or missing references) //IL_0211: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_021a: Unknown result type (might be due to invalid IL or missing references) //IL_023a: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Invalid comparison between Unknown and I4 //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Invalid comparison between Unknown and I4 //IL_012b: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Invalid comparison between Unknown and I4 //IL_00fc: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Invalid comparison between Unknown and I4 //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_01bc: 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; Census c = Measure(gameObject); if (GhostDiag.Enabled) { GhostDiag.Dump(ch, "preRepair"); } string text = default(string); Action val = VisualGate.Decide(ref c, ref text); bool flag = false; string text2 = "none"; try { if ((int)val == 1 || (int)val == 3) { if (VisualGate.NoUsableBody(ref c, 0.05f)) { bool flag2 = BodyFactory.ForceGhostVisuals(ch); bool flag3 = BodyFactory.VisRepair(gameObject); text2 = "forceVisuals(built=" + (flag2 ? "T" : "F") + " visRepair=" + (flag3 ? "T" : "F") + ")"; } else { bool flag4 = BodyFactory.VisRepair(gameObject); text2 = "visRepair(" + (flag4 ? "T" : "F") + ")"; } flag = true; } if (GhostRig.Enabled && GhostRig.Reinit(ch, $"visualPass:{val}")) { text2 = (flag ? (text2 + " + rigReinit") : "rigReinit"); flag = true; } if ((int)val == 2 || (int)val == 3) { Animator componentInChildren = gameObject.GetComponentInChildren(true); if ((Object)(object)componentInChildren != (Object)null) { componentInChildren.Rebind(); componentInChildren.Update(0f); AnimatorCullingMode cullingMode = componentInChildren.cullingMode; componentInChildren.cullingMode = (AnimatorCullingMode)0; SkinnedMeshRenderer val2 = LargestSmr(gameObject); bool flag5 = (Object)(object)val2 != (Object)null && val2.updateWhenOffscreen; if ((Object)(object)val2 != (Object)null) { val2.updateWhenOffscreen = true; } text2 = (flag ? (text2 + " + rebind") : "rebind") + $" + alwaysAnimate(was {cullingMode})" + (((Object)(object)val2 != (Object)null && !flag5) ? " + updateWhenOffscreen" : ""); flag = true; } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SPAWN] visual pass apply threw: " + ex.Message)); } Census c2 = (flag ? Measure(gameObject) : c); Plugin.Log.LogMessage((object)($"[SPAWN] visual '{speciesKey}': gate={val} ({text}) applied={text2} | " + "pre " + GeomStr(in c) + " | post " + GeomStr(in c2) + " | diag " + DiagStr(ch))); if (GhostDiag.Enabled) { GhostDiag.Dump(ch, "postRepair"); GhostDiag.DumpDelayed(ch); } return flag; } private static string GeomStr(in Census c) { return $"renderers={c.Renderers} ready={c.RenderReady} skinnedReady={c.SkinnedReady} baked=({c.BakedX:0.0#}, {c.BakedY:0.0#}, {c.BakedZ:0.0#}) " + string.Format("stacked={0}/{1} anim={2}", c.StackedBones, c.TotalBones, (!c.AnimatorPresent) ? "-" : (c.AnimatorInitialized ? "init" : "PRESENT-uninit")); } internal static SkinnedMeshRenderer LargestSmr(GameObject go) { SkinnedMeshRenderer val = null; if ((Object)(object)go == (Object)null) { return null; } SkinnedMeshRenderer[] componentsInChildren = go.GetComponentsInChildren(true); foreach (SkinnedMeshRenderer val2 in componentsInChildren) { if ((Object)(object)val2 != (Object)null && ((Object)(object)val == (Object)null || val2.bones.Length > val.bones.Length)) { val = val2; } } return val; } internal static string DiagStr(Character ch) { //IL_010d: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)ch == (Object)null) { return "-"; } GameObject gameObject = ((Component)ch).gameObject; Animator componentInChildren = gameObject.GetComponentInChildren(true); SkinnedMeshRenderer val = LargestSmr(gameObject); int num = 0; int num2 = 0; if ((Object)(object)val != (Object)null) { Transform[] bones = val.bones; foreach (Transform val2 in bones) { Rigidbody val3 = (((Object)(object)val2 != (Object)null) ? ((Component)val2).GetComponent() : null); if (!((Object)(object)val3 == (Object)null)) { num2++; if (!val3.isKinematic) { num++; } } } } string text = (((Object)(object)componentInChildren == (Object)null) ? "anim=-" : ("avatarValid=" + (((Object)(object)componentInChildren.avatar != (Object)null && componentInChildren.avatar.isValid) ? "T" : "F") + " isHuman=" + (componentInChildren.isHuman ? "T" : "F") + string.Format(" rootMotion={0} culling={1} animEnabled={2}", componentInChildren.applyRootMotion ? "T" : "F", componentInChildren.cullingMode, ((Behaviour)componentInChildren).enabled ? "T" : "F"))); string text2 = (((Object)(object)val == (Object)null) ? "smr=-" : ("smrVisible=" + (((Renderer)val).isVisible ? "T" : "F") + " updOff=" + (val.updateWhenOffscreen ? "T" : "F"))); string text3 = string.Format("ragdollRoot={0} ragdollActive={1} nonKinematic={2}/{3}", ((Object)(object)ch.RagdollRoot != (Object)null) ? "set" : "null", ch.RagdollActive ? "T" : "F", num, num2); return text + " " + text2 + " " + text3; } catch (Exception ex) { return "threw:" + ex.GetType().Name; } } } internal static class WarmMirror { private struct PeerRow { public WarmSet Set; public float At; public bool WasRefresh; } private const string LocalRowKey = "warm"; private const float PollSeconds = 2f; private const float RefreshSeconds = 30f; private static NetChannel _ch; private static ReplicatedStore _store; private static readonly Dictionary _peers = new Dictionary(); private static bool _dirty = true; private static string _dirtyWhy = "boot"; private static string _lastPublished; private static string _pollEncoded; private static float _pollEncodedAt; private static string _lastPublishWhy = ""; private static float _lastPublishAt; private static float _nextPublishAt; private static float _nextPollAt; private static readonly List _candidates = new List(); private static readonly List _localWarm = new List(); private static bool _tickWarned; private static WantBook _book; private static double _bookTimeout; private static double _bookMaxTimeout; private static int _bookAttempts; private const float WantDriveSeconds = 1f; private static float _nextWantDriveAt; private static bool _driveWarned; private const float RowsCacheSeconds = 0.5f; private static List _rowsCache; private static float _rowsCacheUntil; internal static void Attach(NetChannel ch) { //IL_0017: 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_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Expected O, but got Unknown //IL_0069: Expected O, but got Unknown if (ch != null && _store == null) { _ch = ch; _store = ch.RegisterStore("warm", new StoreOptions { Authority = (StoreAuthority)2, RefreshSeconds = 30f, FlushOnPeerReady = true, ClearOnRoomChange = true, ReapAbsentActors = true, Verbs = new StoreVerbs { Announce = "sk.warmset", Release = "sk.warmclr" } }); _store.OnSet += OnWarmSet; _store.OnCleared += OnWarmCleared; Net.OnRoomChanged += delegate { OnRoomChanged(); }; Net.OnSceneReady += delegate(string scene) { MarkDirty("scene ready " + scene); }; } } internal static void MarkDirty(string why) { _dirty = true; _dirtyWhy = (string.IsNullOrEmpty(why) ? "?" : why); } private static void OnRoomChanged() { _peers.Clear(); _rowsCache = null; _lastPublished = null; _lastPublishWhy = ""; _candidates.Clear(); _pollEncoded = null; _nextPollAt = 0f; if (_book != null) { _book.Clear(); } _nextWantDriveAt = 0f; MarkDirty("room changed"); } private static double CfgTimeout() { return (Plugin.RoomWarmRequestTimeoutSeconds != null) ? Plugin.RoomWarmRequestTimeoutSeconds.Value : 30f; } private static double CfgMaxTimeout() { return (Plugin.RoomWarmRequestMaxTimeoutSeconds != null) ? Plugin.RoomWarmRequestMaxTimeoutSeconds.Value : 120f; } private static int CfgAttempts() { if (Plugin.RoomWarmRequestAttempts == null) { return 3; } return Plugin.RoomWarmRequestAttempts.Value; } private static WantBook Book() { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown double num = CfgTimeout(); double num2 = CfgMaxTimeout(); int num3 = CfgAttempts(); if (_book != null && num == _bookTimeout && num2 == _bookMaxTimeout && num3 == _bookAttempts) { return _book; } _bookTimeout = num; _bookMaxTimeout = num2; _bookAttempts = num3; WantBook val = new WantBook(num, num2, num3); val.CopyTombstonesFrom(_book); _book = val; return _book; } internal static void WantSpeciesOn(int actor, string species) { Book().Want(actor, species, (double)Time.unscaledTime); } internal static bool IsAbandoned(int actor, string species) { if (_book != null) { return _book.IsAbandoned(actor, species); } return false; } internal static int DriveWants() { try { return DriveWantsCore(); } catch (Exception arg) { if (_driveWarned) { return 0; } _driveWarned = true; Plugin.Log.LogWarning((object)("[MIRROR] want sweep threw (warned once per session; the book " + $"retries on the next tick): {arg}")); return 0; } } private static int DriveWantsCore() { //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: 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) //IL_00ce: 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_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Invalid comparison between Unknown and I4 //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_018e: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) //IL_01d4: 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_01f1: 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_01ad: Unknown result type (might be due to invalid IL or missing references) if (_book == null || (_book.Count == 0 && _book.AbandonedCount == 0)) { return 0; } if (!SpawnNet.IsRoomHostRaw()) { return 0; } Dictionary facts = new Dictionary(); List list = PeerRows(); for (int i = 0; i < list.Count; i++) { if ((int)list[i].Status == 2) { facts[list[i].Actor] = list[i]; } } int num = CfgAttempts(); PeerWarmRow value; List list3 = default(List); List list2 = Book().TakeDue((Func?>)((int actor, string species) => (!facts.TryGetValue(actor, out value)) ? (((bool, bool, int)?)null) : new(bool, bool, int)?((SpeciesRoomPolicy.Contains(value.Warm, species), SpeciesRoomPolicy.Contains(value.DeadEnds, species), value.RemainingBudget))), (double)Time.unscaledTime, ref list3); int num2 = 0; for (int num3 = 0; num3 < list2.Count; num3++) { WantEntry val = list2[num3]; bool flag; try { flag = SpawnNet.SendToActor(val.Actor, "sk.want", val.Species); } catch (Exception ex) { Plugin.Log.LogWarning((object)$"[MIRROR] sk.want for '{val.Species}' to actor {val.Actor} threw: {ex.Message}"); continue; } if (flag) { num2++; Plugin.Log.LogMessage((object)($"[MIRROR] want '{val.Species}' → actor {val.Actor} " + $"(attempt {val.Attempts}/{num})")); } } for (int num4 = 0; num4 < list3.Count; num4++) { WantEntry val2 = list3[num4]; if (WasDeadEnd(facts, val2)) { Plugin.Log.LogMessage((object)($"[MIRROR] actor {val2.Actor} cannot warm '{val2.Species}' " + "(dead end / no budget) — not asking again this session")); } else { Plugin.Log.LogMessage((object)($"[MIRROR] actor {val2.Actor} did not warm '{val2.Species}' after " + $"{val2.Attempts} attempts — giving up (not asking again this session)")); } } return num2; } private static bool WasDeadEnd(Dictionary facts, WantEntry e) { //IL_0001: 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_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) if (!facts.TryGetValue(e.Actor, out var value)) { return false; } if (!SpeciesRoomPolicy.Contains(value.DeadEnds, e.Species)) { return value.RemainingBudget <= 0; } return true; } internal static string WantBookDump() { //IL_00c5: 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_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: 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_0122: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_01db: 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_01e8: Unknown result type (might be due to invalid IL or missing references) //IL_01f4: Unknown result type (might be due to invalid IL or missing references) //IL_01fb: Unknown result type (might be due to invalid IL or missing references) List list = ((_book != null) ? _book.Snapshot() : new List()); List list2 = ((_book != null) ? _book.Tombstones() : new List()); if (list.Count == 0 && list2.Count == 0) { return "[MIRROR] want book: (empty)"; } float unscaledTime = Time.unscaledTime; StringBuilder stringBuilder = new StringBuilder(); if (list.Count == 0) { stringBuilder.Append("[MIRROR] want book: (no outstanding asks)"); } if (list.Count > 0) { stringBuilder.Append($"[MIRROR] want book ({list.Count}; timeout {CfgTimeout():F0}s doubling to " + $"{CfgMaxTimeout():F0}s, {CfgAttempts()} attempts):"); } for (int i = 0; i < list.Count; i++) { WantEntry val = list[i]; stringBuilder.Append($" actor {val.Actor} '{val.Species}' attempts={val.Attempts}/{CfgAttempts()}"); stringBuilder.Append((val.Attempts == 0) ? " next=now" : ((val.Attempts >= CfgAttempts()) ? $" abandoned in {val.NextSendAt - (double)unscaledTime:F0}s (its last window)" : $" next={val.NextSendAt - (double)unscaledTime:F0}s")); if (i < list.Count - 1) { stringBuilder.Append(";"); } } if (list2.Count > 0) { stringBuilder.Append($"{Environment.NewLine}[MIRROR] abandoned ({list2.Count}; " + "re-askable only if the peer leaves, the room changes, or it is seen warm):"); for (int j = 0; j < list2.Count; j++) { WantEntry val2 = list2[j]; stringBuilder.Append($" actor {val2.Actor} '{val2.Species}' after {val2.Attempts} attempts"); if (j < list2.Count - 1) { stringBuilder.Append(";"); } } } return stringBuilder.ToString(); } internal static void Tick() { try { TickCore(); } catch (Exception arg) { if (!_tickWarned) { _tickWarned = true; Plugin.Log.LogWarning((object)("[MIRROR] warm-mirror tick threw (warned once per session; publishing " + $"continues on the next tick): {arg}")); } } } private static void TickCore() { //IL_00cb: Unknown result type (might be due to invalid IL or missing references) if (_store == null || !Net.InRoom) { return; } float unscaledTime = Time.unscaledTime; if (unscaledTime >= _nextPollAt) { _nextPollAt = unscaledTime + 2f; RefreshCandidates(); _pollEncoded = Encode(); _pollEncodedAt = unscaledTime; if (!_dirty && !string.Equals(_pollEncoded, _lastPublished, StringComparison.Ordinal)) { MarkDirty("poll found a difference"); } } if (unscaledTime >= _nextWantDriveAt) { _nextWantDriveAt = unscaledTime + 1f; DriveWants(); } if (!_dirty) { return; } float num = ((Plugin.WarmSetPublishSeconds != null) ? Plugin.WarmSetPublishSeconds.Value : 1f); if (num < 0f) { num = 0f; } if (unscaledTime < _nextPublishAt) { return; } _nextPublishAt = unscaledTime + num; string text; if ((int)MirrorEncodeReuse.Choose(_pollEncoded != null, (double)_pollEncodedAt, (double)unscaledTime) == 0) { text = _pollEncoded; } else { RefreshCandidates(); text = Encode(); } _pollEncoded = null; if (_store.Announce("warm", text, "")) { _dirty = false; if (!string.Equals(text, _lastPublished, StringComparison.Ordinal)) { _lastPublished = text; _lastPublishWhy = _dirtyWhy; _lastPublishAt = unscaledTime; } } } private static string BudgetText(int b) { if (b < 99) { return b.ToString(); } return "unlimited"; } private static string Encode() { return WarmSetWire.Encode((IEnumerable)LocalWarmKeys(), (IEnumerable)GuestReplicas.DeadEnds(), GuestReplicas.RemainingBudget()); } internal static IReadOnlyList LocalWarmKeys() { if (_candidates.Count == 0) { RefreshCandidates(); } _localWarm.Clear(); for (int i = 0; i < _candidates.Count; i++) { string text = _candidates[i]; bool flag; try { flag = Spawner.CanMintNow(text); } catch { flag = false; } if (flag) { _localWarm.Add(text); } } return new List(_localWarm); } private static void RefreshCandidates() { List list = new List(_candidates.Count + 4); HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase); try { AddKeys(TemplateStore.Spawnables.LruView, seen, list); AddKeys(TemplateStore.CompanionBodies.LruView, seen, list); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[MIRROR] warm-set candidate scan threw — keeping the previous " + $"{_candidates.Count}-key candidate list: {ex.Message}")); return; } _candidates.Clear(); _candidates.AddRange(list); } private static void AddKeys(IReadOnlyList keys, HashSet seen, List into) { if (keys == null) { return; } for (int i = 0; i < keys.Count; i++) { string text = keys[i]; if (!string.IsNullOrEmpty(text)) { text = text.Trim(); if (text.Length > 0 && seen.Add(text)) { into.Add(text); } } } } private static void OnWarmSet(string key, string payload, RecordMeta meta) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: 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_00e8: 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) //IL_0112: 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_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Unknown result type (might be due to invalid IL or missing references) //IL_015a: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Unknown result type (might be due to invalid IL or missing references) //IL_017e: Unknown result type (might be due to invalid IL or missing references) //IL_018e: Unknown result type (might be due to invalid IL or missing references) if (IsSelf(meta.SenderActor)) { if (Plugin.VerboseNet != null && Plugin.VerboseNet.Value) { Plugin.Log.LogInfo((object)($"[MIRROR] own warm row applied locally (actor={meta.SenderActor}, self) " + (meta.IsRefresh ? "(refresh)" : "(changed)"))); } return; } WarmSet val = WarmSetWire.Decode(payload); if (!val.IsPresent) { Plugin.Log.LogWarning((object)($"[MIRROR] warm row from actor {meta.SenderActor} did not decode " + $"(len={payload?.Length ?? 0}, cap {256} " + "species per field): '" + payload + "' — that peer stays unknown to the room gate. A row is REFUSED rather than truncated: a truncated warm set is a peer claiming to be cold on species it actually holds, which under RoomStrict vetoes them room-wide and reads as a legitimate refusal.")); return; } _peers[meta.SenderActor] = new PeerRow { Set = val, At = Time.unscaledTime, WasRefresh = meta.IsRefresh }; _rowsCache = null; if (!meta.IsRefresh) { DriveWants(); SpawnNet.OnPeerRowGained(meta.SenderActor, val.Warm); } if (Plugin.VerboseNet == null || Plugin.VerboseNet.Value) { Plugin.Log.LogInfo((object)($"[MIRROR] warm row actor={meta.SenderActor} warm={val.Warm.Length} " + $"dead={val.DeadEnds.Length} budget={BudgetText(val.RemainingBudget)} " + (meta.IsRefresh ? "(refresh)" : "(changed)"))); } } private static bool IsSelf(int actor) { if (actor == 0) { return false; } try { return PhotonNetwork.player != null && PhotonNetwork.player.ID == actor; } catch { return false; } } private static void OnWarmCleared(string key, string reason, RecordMeta meta) { //IL_0000: 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_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) if (!IsSelf(meta.SenderActor) && _peers.Remove(meta.SenderActor)) { _rowsCache = null; if (_book != null) { _book.ForgetActor(meta.SenderActor); } SpawnNet.OnPeerRowLost(meta.SenderActor); Plugin.Log.LogMessage((object)$"[MIRROR] warm row actor={meta.SenderActor} dropped — {reason}."); } } internal static List PeerRows() { float unscaledTime = Time.unscaledTime; if (_rowsCache != null && unscaledTime < _rowsCacheUntil) { return _rowsCache; } _rowsCache = BuildPeerRows(); _rowsCacheUntil = unscaledTime + 0.5f; return _rowsCache; } private static List BuildPeerRows() { //IL_0050: 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_0067: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: 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_00b7: 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_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) List list = new List(); if (_ch == null) { return list; } PhotonPlayer[] array = null; try { array = PhotonNetwork.otherPlayers; } catch { } if (array == null) { return list; } PhotonPlayer[] array2 = array; foreach (PhotonPlayer val in array2) { if (val != null) { int iD = val.ID; if (!_ch.IsPeerReady(iD)) { list.Add(new PeerWarmRow { Actor = iD, Status = (PeerWarmStatus)0 }); continue; } if (!_peers.TryGetValue(iD, out var value) || !_ch.IsPeerSceneReady(iD)) { list.Add(new PeerWarmRow { Actor = iD, Status = (PeerWarmStatus)1 }); continue; } list.Add(new PeerWarmRow { Actor = iD, Status = (PeerWarmStatus)2, Warm = value.Set.Warm, DeadEnds = value.Set.DeadEnds, RemainingBudget = value.Set.RemainingBudget }); } } return list; } internal static int ParticipatingCount() { //IL_000e: 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_0019: Invalid comparison between Unknown and I4 int num = 0; List list = PeerRows(); for (int i = 0; i < list.Count; i++) { if ((int)list[i].Status == 2) { num++; } } return num; } internal static string RoomGlyph(string speciesKey, bool localWarm) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Invalid comparison between Unknown and I4 //IL_0031: Unknown result type (might be due to invalid IL or missing references) int num = 0; int num2 = 0; try { if (PhotonNetwork.inRoom && PhotonNetwork.isMasterClient) { List list = PeerRows(); for (int i = 0; i < list.Count; i++) { if ((int)list[i].Status == 2) { num++; if (!SpeciesRoomPolicy.Contains(list[i].Warm, speciesKey)) { num2++; } } } } } catch { num = 0; num2 = 0; } return SpawnMenuLabels.RoomGlyph(localWarm, num, num2); } internal static string Dump() { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0253: Unknown result type (might be due to invalid IL or missing references) //IL_0258: Unknown result type (might be due to invalid IL or missing references) //IL_025a: Unknown result type (might be due to invalid IL or missing references) //IL_025c: Unknown result type (might be due to invalid IL or missing references) //IL_0262: Invalid comparison between Unknown and I4 //IL_0297: Unknown result type (might be due to invalid IL or missing references) //IL_02ac: Unknown result type (might be due to invalid IL or missing references) //IL_026a: Unknown result type (might be due to invalid IL or missing references) //IL_0276: Unknown result type (might be due to invalid IL or missing references) //IL_0278: Unknown result type (might be due to invalid IL or missing references) //IL_02e5: Unknown result type (might be due to invalid IL or missing references) //IL_02fd: Unknown result type (might be due to invalid IL or missing references) //IL_030b: Unknown result type (might be due to invalid IL or missing references) //IL_032d: Unknown result type (might be due to invalid IL or missing references) //IL_0337: Unknown result type (might be due to invalid IL or missing references) //IL_0355: Unknown result type (might be due to invalid IL or missing references) //IL_0365: Unknown result type (might be due to invalid IL or missing references) StringBuilder stringBuilder = new StringBuilder(); float unscaledTime = Time.unscaledTime; stringBuilder.AppendLine(string.Format("[MIRROR] warm mirror: inRoom={0} store={1} ", Net.InRoom, (_store != null) ? "registered" : "NOT registered") + "mode=" + ((Plugin.RoomWarmMode != null) ? ((object)Plugin.RoomWarmMode.Value/*cast due to .constrained prefix*/).ToString() : "?") + " (Phase B: ENFORCED — RoomStrict refuses, RoomDegraded spawns and requests a warm, Local ignores peers)"); IReadOnlyList readOnlyList = LocalWarmKeys(); stringBuilder.AppendLine($"[MIRROR] local: warm={readOnlyList.Count} dead={GuestReplicas.DeadEnds().Count} " + $"budget={BudgetText(GuestReplicas.RemainingBudget())} candidates={_candidates.Count}"); stringBuilder.AppendLine("[MIRROR] local warm: " + ((readOnlyList.Count == 0) ? "(none)" : Join(readOnlyList))); IReadOnlyList readOnlyList2 = GuestReplicas.DeadEnds(); if (readOnlyList2.Count > 0) { stringBuilder.AppendLine("[MIRROR] local dead ends: " + Join(readOnlyList2)); } if (Net.InRoom) { stringBuilder.AppendLine("[MIRROR] local buckets: " + GuestReplicas.BucketsDump()); string text = GuestReplicas.WantedDump(); stringBuilder.AppendLine("[MIRROR] local wanted (head first): " + ((text.Length == 0) ? "(none)" : text)); } stringBuilder.AppendLine((_lastPublished == null) ? $"[MIRROR] never published (dirty={_dirty} why='{_dirtyWhy}')" : ($"[MIRROR] last publish {unscaledTime - _lastPublishAt:F0}s ago, why='{_lastPublishWhy}', " + "pending=" + (_dirty ? ("yes (" + _dirtyWhy + ")") : "no"))); List list = Spawner.RoomWarmSnapshot(); stringBuilder.AppendLine("[MIRROR] room-wide (local ∩ every participating peer): " + ((list.Count == 0) ? "(none)" : Join(list))); stringBuilder.AppendLine(WantBookDump()); List list2 = PeerRows(); if (list2.Count == 0) { stringBuilder.Append("[MIRROR] no other peers in the room."); return stringBuilder.ToString(); } stringBuilder.AppendLine($"[MIRROR] {list2.Count} peer(s):"); for (int i = 0; i < list2.Count; i++) { PeerWarmRow val = list2[i]; if ((int)val.Status != 2) { stringBuilder.AppendLine($"[MIRROR] actor {val.Actor}: {val.Status}"); continue; } _peers.TryGetValue(val.Actor, out var value); stringBuilder.AppendLine($"[MIRROR] actor {val.Actor}: Participating age={unscaledTime - value.At:F0}s" + string.Format("{0} warm={1} ", value.WasRefresh ? " (last landing was a refresh)" : "", val.Warm.Length) + $"dead={val.DeadEnds.Length} budget={BudgetText(val.RemainingBudget)}"); stringBuilder.AppendLine("[MIRROR] warm: " + ((val.Warm.Length == 0) ? "(none)" : Join(val.Warm))); if (val.DeadEnds.Length != 0) { stringBuilder.AppendLine("[MIRROR] dead ends: " + Join(val.DeadEnds)); } } return stringBuilder.ToString().TrimEnd(Array.Empty()); } private static string Join(IReadOnlyList keys) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < keys.Count; i++) { if (stringBuilder.Length > 0) { stringBuilder.Append(", "); } stringBuilder.Append(keys[i]); } return stringBuilder.ToString(); } } }