using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using CompanionKit.Core; using ExitGames.Client.Photon; using ForgeKit; using HarmonyLib; using Microsoft.CodeAnalysis; using NetKit; using NetKit.Core; using UnityEngine; using UnityEngine.Events; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("DonorKit")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.1.0")] [assembly: AssemblyInformationalVersion("0.1.1+c48b2eb6460f1b993d9ddb914cbdc5ff6f1d22c6")] [assembly: AssemblyProduct("DonorKit")] [assembly: AssemblyTitle("DonorKit")] [assembly: AssemblyMetadata("BuildStamp", "c48b2eb 2026-07-30")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.1.0")] [module: UnverifiableCode] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } } namespace DonorKit { [HarmonyPatch(typeof(AmbienceSound), "OnUpdate")] public static class AmbienceSoundGuard { [HarmonyFinalizer] private static Exception Finalizer(Exception __exception, AmbienceSound __instance) { return AmbienceGuard.Handle(__exception, __instance); } } [HarmonyPatch(typeof(AmbienceZone), "OnUpdate")] public static class AmbienceZoneGuard { [HarmonyFinalizer] private static Exception Finalizer(Exception __exception, AmbienceZone __instance) { return AmbienceGuard.Handle(__exception, (AmbienceSound)(object)__instance); } } [HarmonyPatch(typeof(GlobalAudioManager), "ClearPlayers")] public static class ClearPlayersGuard { [HarmonyFinalizer] private static Exception Finalizer(Exception __exception) { if (__exception == null) { return null; } DonorRuntime.Log.LogWarning((object)("[AMBIENCE-GUARD] GlobalAudioManager.ClearPlayers threw (" + __exception.GetType().Name + ": " + __exception.Message + ") — swallowing so it can't abort a scene load/area switch.")); return null; } } [HarmonyPatch(typeof(GlobalAudioManager), "GetUsableSoundInstance")] public static class GetUsableSoundInstanceGuard { private static readonly HashSet _detailed = new HashSet(); private static int _count; private static float _heartbeatAt; [HarmonyFinalizer] private static Exception Finalizer(Exception __exception, GlobalAudioManager __instance, Sounds _sound, ref SplitableSoundSource __result) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) if (__exception == null) { return null; } __result = null; _count++; if (_detailed.Add(_sound)) { DonorRuntime.Log.LogWarning((object)$"[AMBIENCE-RECON] GetUsableSoundInstance threw ({__exception.GetType().Name}: {__exception.Message}) for sound '{_sound}'. {DescribeSoundState(__instance, _sound)}\n{__exception.StackTrace}"); _heartbeatAt = Time.unscaledTime; } else if (Time.unscaledTime - _heartbeatAt > 10f) { _heartbeatAt = Time.unscaledTime; DonorRuntime.Log.LogWarning((object)$"[AMBIENCE-GUARD] GetUsableSoundInstance still failing — {_count} swallowed so far; latest sound '{_sound}'."); } return null; } private static string DescribeSoundState(GlobalAudioManager mgr, Sounds sound) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0024: 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) //IL_00cf: 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_00f3: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)mgr == (Object)null) { return "(no manager instance)"; } try { string arg = ((!mgr.m_soundPrefabs.ContainsKey(sound)) ? "absent" : (((Object)(object)mgr.m_soundPrefabs[sound] == (Object)null) ? "DESTROYED" : "ok")); string arg2 = "absent"; if (mgr.m_soundInstances.ContainsKey(sound)) { List list = mgr.m_soundInstances[sound]; int num = 0; foreach (SplitableSoundSource item in list) { if (item != null && (Object)(object)item == (Object)null) { num++; } } arg2 = $"{list.Count} (dead {num})"; } return $"state: prefab={arg} busyKey={mgr.BusySounds.ContainsKey(sound)} instances={arg2} " + $"mixerKey={mgr.MixerGroupPerSound != null && mgr.MixerGroupPerSound.ContainsKey(sound)} poolAlive={(Object)(object)mgr.m_soundPool != (Object)null}"; } catch (Exception ex) { return "(state probe failed: " + ex.Message + ")"; } } } public static class AmbienceGuard { public static Exception Handle(Exception exception, AmbienceSound instance) { if (exception == null) { return null; } DonorRuntime.Log.LogWarning((object)("[AMBIENCE-GUARD] '" + PathOf((Component)(object)instance) + "' (" + ((object)instance)?.GetType().Name + ") OnUpdate threw (" + exception.GetType().Name + ": " + exception.Message + ") — disabling it so it stops retrying every frame.")); try { if ((Object)(object)instance != (Object)null) { ((Behaviour)instance).enabled = false; } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[AMBIENCE-GUARD] disable failed: " + ex.Message)); } try { if ((Object)(object)instance != (Object)null && DonorHarvest.RemoveFromAudioRegistries(instance)) { DonorRuntime.Log.LogWarning((object)("[AMBIENCE-GUARD] '" + PathOf((Component)(object)instance) + "' also unregistered from GlobalAudioManager — manager-driven .Play() calls would otherwise keep retrying it after the disable.")); } } catch (Exception ex2) { DonorRuntime.Log.LogWarning((object)("[AMBIENCE-GUARD] unregister failed: " + ex2.Message)); } return null; } public static string PathOf(Component c) { //IL_0053: 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) try { if ((Object)(object)c == (Object)null) { return ""; } string text = ((Object)c.gameObject).name; Transform parent = c.transform.parent; while ((Object)(object)parent != (Object)null) { text = ((Object)parent).name + "/" + text; parent = parent.parent; } Scene scene = c.gameObject.scene; return ((Scene)(ref scene)).name + ":" + text; } catch { return ""; } } } public sealed class BodyTemplate { public GameObject Dormant; public string Key; public string SpeciesId; public CreatureAttributes Captured; public bool Substituted; } public static class BodyTemplateStore { public static Func StatCapturer; private const int CacheWarnThreshold = 24; private static int _lastCensusWarnCount = -1; private static readonly HashSet _warnedSubs = new HashSet(StringComparer.OrdinalIgnoreCase); private static TemplateKind Kind => TemplateStore.CompanionBodies; public static int Count { get { Prune(); return Kind.RawCount; } } public static bool TryResolve(string speciesId, out BodyTemplate template) { Prune(); template = null; if (string.IsNullOrEmpty(speciesId)) { return false; } string text = default(string); if (!SpeciesTable.TryResolveKey(Kind.Table, speciesId, ref text, ref template, (string)null) || template == null) { return false; } if (!StatAdoption.IsSameSpecies(speciesId, template.SpeciesId)) { WarnSubstitutionOnce(speciesId, template.SpeciesId); } return true; } public static bool TryResolveExact(string speciesId, out BodyTemplate template) { Prune(); template = null; if (string.IsNullOrEmpty(speciesId)) { return false; } if (!Kind.TryGet(speciesId.Trim(), out template) || template == null) { return false; } if (template.Substituted) { WarnSubstitutionOnce(speciesId, template.SpeciesId); template = null; return false; } return true; } public static BodyTemplate GetResident(string key) { Prune(); if (string.IsNullOrEmpty(key)) { return null; } if (!Kind.TryGet(key.Trim(), out var entry)) { return null; } return entry; } private static void WarnSubstitutionOnce(string asked, string got) { if (_warnedSubs.Add(asked + "→" + got)) { DonorRuntime.Log.LogWarning((object)("[TEMPLATE] SUBSTITUTION: asked for '" + asked + "', but the cached body is a '" + got + "' — NOT the same creature (different stats, possibly a different model; e.g. Elite Gargoyle Mage 1700hp vs Gargoyle 750hp). A PET may still wear it as a fallback body, but it will never be recorded as its identity (Bug 31). A SPAWN refuses it outright. If '" + asked + "' matters, it needs its own donor row + a capture; see Bug 28.")); } } public static void ResetSubstitutionWarnings() { _warnedSubs.Clear(); } public static BodyTemplate Capture(Character src, string key, object statHost = null) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)src == (Object)null || string.IsNullOrEmpty(key)) { return null; } GameObject val = null; try { val = Object.Instantiate(((Component)src).gameObject, Kind.Holder().transform); ((Object)val).name = "CK_BodyTemplate_" + key; val.transform.localPosition = Vector3.zero; PhotonView component = val.GetComponent(); if ((Object)(object)component != (Object)null) { component.viewID = 0; } AISquadMember[] componentsInChildren = val.GetComponentsInChildren(true); foreach (AISquadMember val2 in componentsInChildren) { if ((Object)(object)val2 != (Object)null && (Object)(object)val2.AISquad != (Object)null) { val2.AISquad = null; } } string text = RagdollRig.RestoreJoints(((Component)src).gameObject, val, delegate(string w) { DonorRuntime.Log.LogWarning((object)("[TEMPLATE] ragdoll ('" + key + "'): " + w)); }); if (text != null) { DonorRuntime.Log.LogMessage((object)("[TEMPLATE] ragdoll ('" + key + "'): " + text + ".")); } RigCheck(((Component)src).gameObject, val, key); string text2 = DonorHarvest.IdentityFor(key, src); BodyTemplate bodyTemplate = new BodyTemplate { Dormant = val, Key = key, SpeciesId = text2, Captured = ((StatCapturer != null) ? StatCapturer(src, statHost) : null), Substituted = !StatAdoption.IsSameSpecies(key, text2) }; if (Kind.TryGet(key, out var entry) && (Object)(object)entry?.Dormant != (Object)null && (Object)(object)entry.Dormant != (Object)(object)val) { Object.Destroy((Object)(object)entry.Dormant); } Kind.Put(key, bodyTemplate); DonorRuntime.Log.LogMessage((object)("[TEMPLATE] cached body template '" + key + "' (species '" + bodyTemplate.SpeciesId + "'" + (bodyTemplate.Substituted ? (" — SUBSTITUTE: this is NOT a '" + key + "'") : "") + ", stats " + ((bodyTemplate.Captured != null) ? "captured" : "UNREADABLE — config fallback at build") + ").")); if (bodyTemplate.Substituted) { DonorRuntime.Log.LogWarning((object)("[TEMPLATE] '" + key + "' was captured from a '" + text2 + "' donor — the two are different creatures. A spawn of this key will now REFUSE the cache and harvest honestly; a pet may still wear it as a fallback body but will never record it as its identity (Bug 31). Fix: give '" + key + "' its own donor row, or pin it (Bug 28).")); } if (Kind.RawCount > 0 && Kind.RawCount % 24 == 0 && Kind.RawCount != _lastCensusWarnCount) { _lastCensusWarnCount = Kind.RawCount; DonorRuntime.Log.LogWarning((object)($"[TEMPLATE] cache now holds {Kind.RawCount} resident body template(s) — " + "each pins its mesh/texture assets in memory. 'templateclear' frees them (safe when no re-form is mid-build).")); } return bodyTemplate; } catch (Exception ex) { DonorRuntime.Log.LogError((object)("[TEMPLATE] capture of '" + key + "' threw — destroying the orphan clone: " + ex.Message)); if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } return null; } } private static void RigCheck(GameObject donor, GameObject clone, string key) { Action warn = delegate(string w) { DonorRuntime.Log.LogWarning((object)("[RIG] ('" + key + "') " + w)); }; SkeletonRig.Report report = SkeletonRig.Audit(donor, clone, warn); DonorRuntime.Log.LogMessage((object)("[TEMPLATE] rig ('" + key + "'): " + report.Summary + ".")); if (report.Healthy) { return; } SkeletonRig.LogForensics(report, donor, warn); if (!SkeletonRig.RepairEnabled) { DonorRuntime.Log.LogWarning((object)("[TEMPLATE] rig ('" + key + "'): external references present and [Rig] RepairSkinnedBones=false (log-only) — puppets from this template will mis-render.")); return; } string text = SkeletonRig.Repair(donor, clone, report, warn); SkeletonRig.Report report2 = SkeletonRig.Audit(null, clone, warn); DonorRuntime.Log.LogMessage((object)("[TEMPLATE] rig repair ('" + key + "'): " + text + " — post: " + report2.Summary + ".")); if (!report2.Healthy) { DonorRuntime.Log.LogWarning((object)("[TEMPLATE] rig ('" + key + "'): still unhealthy after the repair pass — keeping the template anyway (no chain to advance here; a mis-skinned body beats none).")); } } public static int Clear() { _lastCensusWarnCount = -1; return Kind.ClearAll(); } public static string Dump() { Prune(); if (Kind.RawCount == 0) { return "[TEMPLATE] body-template cache empty."; } StringBuilder stringBuilder = new StringBuilder($"[TEMPLATE] {Kind.RawCount} cached body template(s):"); foreach (KeyValuePair entry in Kind.Entries) { stringBuilder.Append("\n '" + entry.Key + "' -> species '" + entry.Value.SpeciesId + "' stats=" + ((entry.Value.Captured != null) ? "captured" : "none") + (entry.Value.Substituted ? (" *** SUBSTITUTE — NOT a '" + entry.Key + "' (spawns refuse it; Bug 31) ***") : "")); } return stringBuilder.ToString(); } public static string Probe() { Prune(); if (Kind.RawCount == 0) { return "[TEMPLATE] body-template cache empty — nothing to probe."; } StringBuilder stringBuilder = new StringBuilder($"[TEMPLATE] mesh probe over {Kind.RawCount} cached template(s):"); foreach (KeyValuePair entry in Kind.Entries) { string text; try { SkinnedMeshRenderer val = (((Object)(object)entry.Value.Dormant != (Object)null) ? entry.Value.Dormant.GetComponentInChildren(true) : null); text = (((Object)(object)val == (Object)null) ? "no SkinnedMeshRenderer" : (((Object)(object)val.sharedMesh == (Object)null) ? "SkinnedMeshRenderer with no sharedMesh" : $"mesh '{((Object)val.sharedMesh).name}' isReadable={val.sharedMesh.isReadable}")); } catch (Exception ex) { text = "probe threw: " + ex.GetType().Name + ": " + ex.Message; } stringBuilder.Append("\n '" + entry.Key + "' (species '" + entry.Value.SpeciesId + "') -> " + text); } return stringBuilder.ToString(); } public static void Drop(string key) { Kind.Remove(key); } private static void Prune() { List list = null; foreach (KeyValuePair entry in Kind.Entries) { if ((Object)(object)entry.Value?.Dormant == (Object)null) { List obj = list ?? new List(); list = obj; obj.Add(entry.Key); } } if (list == null) { return; } foreach (string item in list) { Kind.Remove(item); DonorRuntime.Log.LogWarning((object)("[TEMPLATE] cached body template '" + item + "' was destroyed — dropped from the cache.")); } } } public static class DkConfig { public static class Rig { public static ConfigEntry RepairSkinnedBones; internal static void Bind(ConfigFile cfg) { RepairSkinnedBones = cfg.Bind("Rig", "RepairSkinnedBones", true, "Repair SkinnedMeshRenderer bones that reference transforms OUTSIDE the harvested creature's own hierarchy (they die with the donor scene and the spawn renders as a stretched vertical line — finding F2, Elder Medyse). Runs at template capture while the donor is still loaded: rebind to a same-named internal bone where one exists, else graft the foreign skeleton subtree under the clone. false = log-only (the '[TEMPLATE] rig' audit line still fires)."); } } public static class Expedition { public static ConfigEntry CaptureOnSceneEntry; public static ConfigEntry AutoWarmAtBoot; public static ConfigEntry AlwaysWarmSpecies; public static ConfigEntry AutoWarmRetrySeconds; public static ConfigEntry ReturnRetrySeconds; public static ConfigEntry AllowCoop; public static ConfigEntry GuestAutoContinue; internal static void Bind(ConfigFile cfg) { CaptureOnSceneEntry = cfg.Bind("Expedition", "CaptureOnSceneEntry", false, "When you enter an OVERSIZED scene (region terrain/town — the expedition tier) that still has uncached donor-table species, run the expedition capture payload IN PLACE at player-ready (the scene is already loaded, so it costs zero loading screens) and record the scene in ck_expeditions.txt. Never fires for ordinary scenes (one string check); a fully cached region costs one key sweep. DEFAULT OFF as of the public release: this is the only path on which the kit does heavy work the player never asked for (it clones every uncached donor species in the region on EVERY region/town entry), and it is the suspected source of the un-root-caused 'mystery live Pearlbird' (a live donor engaging the player in combat ~1.2 km away — spawnkit-testplan.md watch-for). Turn it on to pay expedition costs opportunistically instead of via an explicit 'expedition '."); AutoWarmAtBoot = cfg.Bind("Expedition", "AutoWarmAtBoot", "needed", "One-time template-cache warm at the FIRST gameplay player-ready of each launch. 'needed' = only when the active companion's species has expedition-tier donors, NO additive donor, and no cached template — one hands-off round trip to its first candidate (a species with any wild/additive source never triggers anything). 'all' = re-dump every ck_expeditions.txt scene that still has >=1 uncached species (sequential trips). 'off' = never. Unknown values warn and behave as off. AlwaysWarmSpecies is evaluated at the same pass, ungated by the mode but gated by the active companion (see its own description — Cobalt's D8 ruling)."); AlwaysWarmSpecies = cfg.Bind("Expedition", "AlwaysWarmSpecies", "", "Comma-separated species warmed at the same once-per-launch boot evaluation regardless of AutoWarmAtBoot's mode. EMPTY (the default) = the species registered in code by installed consumer mods apply (Beastwhispering registers Pearlbird — its only donor scene is expedition-tier, so a fresh install has a body waiting before its first re-form); a NON-EMPTY list REPLACES the registered defaults; the literal 'none' disables warming entirely (registered defaults included). SINCE 2026-07-21 (Cobalt's D8 ruling): the effective list applies only on a PETLESS launch, or when the active companion's species is itself listed — a bonded save of another species never pays the boot trip. Each listed species with expedition-tier donors and no cached template gets its first candidate scene warmed; species sharing a scene share ONE trip (deduped against the mode's trips too); a species with an additive donor is skipped with a notice (the normal harvest chain covers it); an unrecognized name warns loudly."); AutoWarmRetrySeconds = cfg.Bind("Expedition", "AutoWarmRetrySeconds", 60f, "The boot auto-warm can fire while a vanilla area load or a companion donor harvest is still in flight, and an expedition trip can't START then. Rather than abandoning the whole launch's warm pass, the auto-warm polls (once a second, unscaled) until the loader settles, then re-attempts — for at most this many seconds. 0 = never wait (blocked at boot = give up, the pre-fix behavior); the once-per-launch latch always means ONE pass (a settled launch OR an exhausted wait), never one attempt."); ReturnRetrySeconds = cfg.Bind("Expedition", "ReturnRetrySeconds", 20f, "Safety net for the expedition's RETURN leg (Bug 25 — an expedition that fails to come home leaves you in the donor region, and the game bakes that into the save). RequestSwitchArea has no failure signal, so if the return was asked for and this many seconds later no load has started and we are still standing in the donor scene, the return is re-requested (twice at most, then the watchdog makes one final rescue attempt). This measures request → load-START, not load duration, so a healthy 15-60s region load never trips it. 0 = never retry (not recommended; the watchdog rescue still applies)."); AllowCoop = cfg.Bind("Expedition", "AllowCoop", false, "Allow an expedition to START while other players are connected (a Photon room with guests). DEFAULT FALSE: each of the trip's two chained area switches ends in a PER-MACHINE 'press any key' continue gate that nothing host-side can pass for a guest, and a guest whose gate reader dies (or who is simply AFK) wedges the WHOLE party mid-trip — the 300s watchdog runs inside the same stuck vanilla loader coroutine and cannot recover it, worst case stranding everyone in the donor region (health-review 2026-07-18 §2.4). Solo/offline play never consults this key. Flip to true only for live co-op testing, ideally with GuestAutoContinue=true on every guest's machine."); GuestAutoContinue = cfg.Bind("Expedition", "GuestAutoContinue", true, "GUEST-side safety for co-op expeditions (only ever acts while a HOST-run expedition is in flight — the host stamps the Photon room property CK_EXPED in Begin and clears it in Finish, so this key is inert in ordinary play and safe to leave on). While that property is present and this client sits at a loading continue gate, the gate is auto-passed exactly the way the host's own AutoContinue passes its gates: same conditions, same stale-flag guard (only after this machine's OWN gameplay-loading-done for the current load), through the same public SetContinueAfterLoading seam a keypress uses. Meaningful only when the host set AllowCoop=true."); } } internal static void BindAll(ConfigFile cfg) { Rig.Bind(cfg); Expedition.Bind(cfg); } } internal static class DonorAudioRegistry { private static FieldInfo _timelessField; private static FieldInfo _startTimesField; private static FieldInfo _endTimesField; private static FieldInfo _zonesWaitingField; private static bool _reflectionReady; private static void EnsureReflection() { if (!_reflectionReady) { _reflectionReady = true; Type typeFromHandle = typeof(GlobalAudioManager); _timelessField = typeFromHandle.GetField("m_timelessAmbientSounds", BindingFlags.Instance | BindingFlags.NonPublic); _startTimesField = typeFromHandle.GetField("m_ambienceStartTimes", BindingFlags.Instance | BindingFlags.NonPublic); _endTimesField = typeFromHandle.GetField("m_ambienceEndTimes", BindingFlags.Instance | BindingFlags.NonPublic); _zonesWaitingField = typeFromHandle.GetField("m_ambienceZonesWaitingForTarget", BindingFlags.Instance | BindingFlags.NonPublic); if (_timelessField == null || _startTimesField == null || _endTimesField == null) { DonorRuntime.Log.LogWarning((object)"[HARVEST] GlobalAudioManager ambience field(s) not found by reflection — audio-leak fix may be incomplete."); } } } internal static void DetachAmbience(Scene donor) { GlobalAudioManager audioManager = Global.AudioManager; if ((Object)(object)audioManager == (Object)null) { return; } int num = 0; GameObject[] rootGameObjects = ((Scene)(ref donor)).GetRootGameObjects(); foreach (GameObject val in rootGameObjects) { AmbienceSound[] componentsInChildren = val.GetComponentsInChildren(true); foreach (AmbienceSound val2 in componentsInChildren) { if (!((Object)(object)val2 == (Object)null)) { RemoveFromAudioRegistries(val2); num++; } } } if (num > 0) { DonorRuntime.Log.LogMessage((object)$"[HARVEST] detached {num} ambience source(s) from GlobalAudioManager before touching the donor."); } } internal static bool RemoveFromAudioRegistries(AmbienceSound a) { if ((Object)(object)a == (Object)null) { return false; } GlobalAudioManager audioManager = Global.AudioManager; if ((Object)(object)audioManager == (Object)null) { return false; } EnsureReflection(); List list = _timelessField?.GetValue(audioManager) as List; object obj = _startTimesField?.GetValue(audioManager); object obj2 = _endTimesField?.GetValue(audioManager); List list2 = _zonesWaitingField?.GetValue(audioManager) as List; MethodInfo methodInfo = obj?.GetType().GetMethod("Remove"); try { if (a.IsPlaying) { audioManager.LockAmbience(a); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[HARVEST] LockAmbience failed: " + ex.Message)); } list?.Remove(a); methodInfo?.Invoke(obj, new object[1] { a }); methodInfo?.Invoke(obj2, new object[1] { a }); audioManager.m_originalAmbienceZones.RemoveAll((AmbienceZone z) => (Object)(object)z == (Object)(object)a); list2?.RemoveAll((AmbienceZone z) => (Object)(object)z == (Object)(object)a); return true; } internal static void PruneDeadSoundPlayers() { try { List players = GlobalAudioManager.m_players; if (players != null) { int count = players.Count; players.RemoveAll((SoundPlayer p) => (Object)(object)p == (Object)null); int num = count - players.Count; if (num > 0) { DonorRuntime.Log.LogMessage((object)string.Format("[AMBIENCE-FIX] pruned {0} dead SoundPlayer entr{1} from GlobalAudioManager.m_players ({2} live remain).", num, (num == 1) ? "y" : "ies", players.Count)); } } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[AMBIENCE-FIX] SoundPlayer prune failed: " + ex.Message)); } } internal static void AuditAudioRegistries(string context) { GlobalAudioManager audioManager = Global.AudioManager; if ((Object)(object)audioManager == (Object)null) { DonorRuntime.Log.LogMessage((object)("[AMBIENCE-RECON] (" + context + ") GlobalAudioManager is " + ((audioManager != null) ? "DESTROYED — dangling singleton (bug-2 hijack signature)!" : "null (never set)") + ".")); return; } try { EnsureReflection(); List list = _timelessField?.GetValue(audioManager) as List; List list2 = _zonesWaitingField?.GetValue(audioManager) as List; List originalAmbienceZones = audioManager.m_originalAmbienceZones; List players = GlobalAudioManager.m_players; DonorRuntime.Log.LogMessage((object)("[AMBIENCE-RECON] (" + context + ") timeless=" + Census(list) + " zonesWaiting=" + Census(list2) + " originalZones=" + Census(originalAmbienceZones) + " players=" + Census(players))); LogDeadEntries("timeless", list); LogDeadEntries("zonesWaiting", list2); LogDeadEntries("originalZones", originalAmbienceZones); LogDeadEntries("players", players); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[AMBIENCE-RECON] (" + context + ") audit failed: " + ex.Message)); } } private static string Census(List list) where T : Object { if (list == null) { return "?"; } int num = 0; foreach (T item in list) { if (item != null && (Object)(object)item == (Object)null) { num++; } } return $"{list.Count} (dead {num})"; } private static void LogDeadEntries(string label, List list) where T : Object { if (list == null) { return; } for (int i = 0; i < list.Count; i++) { T val = list[i]; if (val != null && (Object)(object)val == (Object)null) { DonorRuntime.Log.LogWarning((object)$"[AMBIENCE-RECON] DEAD entry in {label}[{i}] (type {((object)val).GetType().Name}) — this is the leak family behind wedged audio."); } } } } internal static class DonorCreatureFinder { internal static Character FindLiveInScene(Scene scene, string creatureName) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) int rank; Character val = FindLiveInScene(scene, creatureName, out rank); if (rank == 1 && (Object)(object)val != (Object)null) { DonorRuntime.Log.LogMessage((object)("[HARVEST] no exact-name donor for '" + creatureName + "' in '" + ((Scene)(ref scene)).name + "' — using nearest substring match '" + val.Name + "'.")); } return val; } internal static Character FindLiveInScene(Scene scene, string creatureName, out int rank) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) string text = DonorSceneTable.PinFor(creatureName); if (text != null) { return FindPinnedInScene(scene, creatureName, text, out rank); } Character val = null; GameObject[] rootGameObjects = ((Scene)(ref scene)).GetRootGameObjects(); foreach (GameObject val2 in rootGameObjects) { Character[] componentsInChildren = val2.GetComponentsInChildren(true); foreach (Character val3 in componentsInChildren) { if ((Object)(object)val3 == (Object)null || !val3.IsAI) { continue; } int num = Species.MatchRank(val3.Name, creatureName); if (num == 0) { continue; } bool? flag = SafeAlive(val3); if (flag != true) { DonorRuntime.Log.LogWarning((object)("[HARVEST] donor '" + val3.Name + "' matched but is " + ((flag == false) ? "DEAD" : "uninitialized (inactive/never awoke — stats unreadable)") + " in '" + ((Scene)(ref scene)).name + "' — skipping (bug-6).")); } else { if (num == 2) { rank = 2; return val3; } if ((Object)(object)val == (Object)null) { val = val3; } } } } rank = (((Object)(object)val != (Object)null) ? 1 : 0); return val; } private static Character FindPinnedInScene(Scene scene, string creatureName, string pin, out int rank) { GameObject[] rootGameObjects = ((Scene)(ref scene)).GetRootGameObjects(); foreach (GameObject val in rootGameObjects) { Character[] componentsInChildren = val.GetComponentsInChildren(true); foreach (Character val2 in componentsInChildren) { if (!((Object)(object)val2 == (Object)null) && val2.IsAI && DonorTable.PinMatches(((Object)((Component)val2).gameObject).name, pin)) { bool? flag = SafeAlive(val2); if (flag == true) { DonorRuntime.Log.LogMessage((object)("[HARVEST] '" + creatureName + "' → pinned donor GameObject '" + ((Object)((Component)val2).gameObject).name + "' in '" + ((Scene)(ref scene)).name + "' (its frozen Character.Name reads '" + val2.Name + "' — bug-28 pin).")); rank = 3; return val2; } DonorRuntime.Log.LogWarning((object)("[HARVEST] pinned donor '" + pin + "' for '" + creatureName + "' matched but is " + ((flag == false) ? "DEAD" : "uninitialized (inactive/never awoke — stats unreadable)") + " in '" + ((Scene)(ref scene)).name + "' — skipping (bug-6).")); } } } DonorRuntime.Log.LogWarning((object)("[HARVEST] '" + creatureName + "' is PINNED to donor GameObject '" + pin + "', which is not in '" + ((Scene)(ref scene)).name + "' — no name fallback (a pin exists precisely because the names lie here). Check the pin against the scene roster below / 'donorspot'.")); rank = 0; return null; } internal static void DumpSceneContents(Scene scene) { GameObject[] rootGameObjects = ((Scene)(ref scene)).GetRootGameObjects(); foreach (GameObject val in rootGameObjects) { DonorRuntime.Log.LogMessage((object)$"[HARVEST] root: '{((Object)val).name}' active={val.activeSelf} children={val.transform.childCount}"); } int num = 0; GameObject[] rootGameObjects2 = ((Scene)(ref scene)).GetRootGameObjects(); foreach (GameObject val2 in rootGameObjects2) { Character[] componentsInChildren = val2.GetComponentsInChildren(true); foreach (Character val3 in componentsInChildren) { num++; string text; try { bool? flag = SafeAlive(val3); text = string.Format("name='{0}' go='{1}' isAI={2} active={3} alive={4}", val3.Name, ((Object)((Component)val3).gameObject).name, val3.IsAI, ((Component)val3).gameObject.activeInHierarchy, flag.HasValue ? flag.Value.ToString() : "?"); } catch (Exception ex) { text = "go='" + (((Object)(object)val3 != (Object)null && (Object)(object)((Component)val3).gameObject != (Object)null) ? ((Object)((Component)val3).gameObject).name : "?") + "' — state unreadable (" + ex.GetType().Name + ")"; } DonorRuntime.Log.LogMessage((object)("[HARVEST] Character: " + text)); } } DonorRuntime.Log.LogMessage((object)$"[HARVEST] {num} Character component(s) total in donor scene."); } private static bool? SafeAlive(Character c) { try { return c.Alive; } catch { return null; } } } internal static class DonorDisplayRepair { private static int _harvestCount; private static CameraCallback _sentinel; private const int EcOwnedCount = 3; private static readonly string[] WaterGlobalNames = new string[6] { "_GlobalWetness", "_GlobalWaterLevel", "_GlobalRainIntensity", "_UBER_GlobalDry", "_UBER_GlobalDryConst", "_UBER_GlobalRainDamp" }; internal static void RestoreSkybox(Material original, string phase) { bool flag = phase != "load-done"; Material skybox = RenderSettings.skybox; if ((Object)(object)original == (Object)null) { if ((Object)(object)skybox != (Object)null && !flag) { DonorRuntime.Log.LogWarning((object)"[SKY-FIX] no skybox material existed pre-load but one does now — leaving it in place."); } return; } if ((Object)(object)skybox == (Object)(object)original) { if (!flag) { DonorRuntime.Log.LogMessage((object)$"[SKY-FIX] no hijack at {phase} (skybox #{((Object)original).GetInstanceID()} unchanged) — but the donor's Start() may not have run yet; later passes re-check."); } return; } RenderSettings.skybox = original; if (flag) { DonorRuntime.Log.LogWarning((object)("[SKY-FIX-LATE] skybox was clobbered AFTER the load-done restore (caught at " + phase + ") — Start()-timing hole CONFIRMED; real material restored.")); } else { DonorRuntime.Log.LogMessage((object)"[SKY-FIX] donor EnvironmentConditions clobbered the global skybox material — restored the real one (black-screen fix)."); } } internal static IEnumerator DeferredSkyCheck(Material original, float[] waterGlobals) { yield return (object)new WaitForSecondsRealtime(2f); RepairDisplayChain("deferred+2s"); if ((Object)(object)original != (Object)null) { RestoreSkybox(original, "deferred+2s"); SkySnapshot.Log("deferred+2s", DonorRuntime.Log); } CheckWaterGlobals(waterGlobals, "deferred+2s", restore: false); int harvestNo = ++_harvestCount; TerrainGuard.Snapshot($"harvest #{harvestNo} pre-purge"); bool purgeRan = TerrainGuard.ShouldPurgeThisHarvest(harvestNo); if (purgeRan) { float p0 = Time.realtimeSinceStartup; yield return Resources.UnloadUnusedAssets(); DonorRuntime.Log.LogMessage((object)$"[HARVEST] post-harvest unused-asset purge done in {Time.realtimeSinceStartup - p0:F2}s (harvest #{harvestNo})."); } else { DonorRuntime.Log.LogMessage((object)$"[HARVEST] unused-asset purge SKIPPED for harvest #{harvestNo} ({TerrainGuard.UnloadModeDescription()}) — donor assets stay resident (terrain-hole mitigation, docs/terrain-hole-plan.md)."); } TerrainGuard.PostHarvestMaintenance($"harvest #{harvestNo}", purgeRan); } internal static int NeutralizeCamerasUnder(GameObject root, string phase) { int num = 0; try { Camera[] componentsInChildren = root.GetComponentsInChildren(true); foreach (Camera val in componentsInChildren) { if (!((Object)(object)val == (Object)null)) { bool enabled = ((Behaviour)val).enabled; bool flag = (Object)(object)val.targetTexture != (Object)null; ((Behaviour)val).enabled = false; if (flag) { val.targetTexture = null; } num++; DonorRuntime.Log.LogMessage((object)$"[DISPLAY-FIX] neutralized donor camera '{((Object)val).name}' ({phase}; was enabled={enabled}, hadTargetTexture={flag})."); } } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[DISPLAY-FIX] camera neutralization under '" + ((Object)root).name + "' failed at " + phase + ": " + ex.Message)); } return num; } internal static void NeutralizeDonorScene(Scene donor, string phase) { try { GameObject[] rootGameObjects = ((Scene)(ref donor)).GetRootGameObjects(); foreach (GameObject val in rootGameObjects) { NeutralizeCamerasUnder(val, phase); MonoBehaviour[] componentsInChildren = val.GetComponentsInChildren(true); foreach (MonoBehaviour val2 in componentsInChildren) { if (!((Object)(object)val2 == (Object)null)) { string name = ((object)val2).GetType().Name; if (DisplayChainRisk.IsWaterReflectionRigName(name) || DisplayChainRisk.IsWaterReflectionRigName(((Object)((Component)val2).gameObject).name)) { bool enabled = ((Behaviour)val2).enabled; ((Behaviour)val2).enabled = false; DonorRuntime.Log.LogMessage((object)$"[DISPLAY-FIX] neutralized donor water-reflection rig '{name}' on '{((Object)((Component)val2).gameObject).name}' ({phase}; was enabled={enabled})."); } } } } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[DISPLAY-FIX] donor-scene neutralization failed at " + phase + ": " + ex.Message)); } } internal static void ArmCameraSentinel(string donorSceneName) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected O, but got Unknown //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Expected O, but got Unknown DisarmCameraSentinel(); _sentinel = (CameraCallback)delegate(Camera cam) { //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) try { if (!((Object)(object)cam == (Object)null)) { Scene scene = ((Component)cam).gameObject.scene; if (!(((Scene)(ref scene)).name != donorSceneName)) { ((Behaviour)cam).enabled = false; cam.targetTexture = null; DonorRuntime.Log.LogWarning((object)("[DISPLAY-FIX] donor camera '" + ((Object)cam).name + "' ATTEMPTED TO RENDER inside the harvest window (onPreCull) — disabled mid-flight (V32-b camera-theory POSITIVE).")); } } } catch { } }; Camera.onPreCull = (CameraCallback)Delegate.Combine((Delegate?)(object)Camera.onPreCull, (Delegate?)(object)_sentinel); } internal static void DisarmCameraSentinel() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown if (_sentinel != null) { Camera.onPreCull = (CameraCallback)Delegate.Remove((Delegate?)(object)Camera.onPreCull, (Delegate?)(object)_sentinel); _sentinel = null; } } private static float EcAuthority(EnvironmentConditions ec, int i) { return i switch { 1 => ec.WaterLevel, 0 => ec.Wetness, _ => ec.Rain, }; } internal static float[] CaptureWaterGlobals() { try { float[] array = new float[WaterGlobalNames.Length]; for (int i = 0; i < WaterGlobalNames.Length; i++) { array[i] = Shader.GetGlobalFloat(WaterGlobalNames[i]); } return array; } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[WATER-FIX] pre-harvest water-globals capture failed: " + ex.Message)); return null; } } internal static void CheckWaterGlobals(float[] captured, string phase, bool restore) { if (captured == null) { return; } try { EnvironmentConditions instance = EnvironmentConditions.Instance; bool flag = false; for (int i = 0; i < WaterGlobalNames.Length; i++) { float globalFloat = Shader.GetGlobalFloat(WaterGlobalNames[i]); if (Mathf.Approximately(globalFloat, captured[i])) { continue; } if (i < 3) { if ((Object)(object)instance != (Object)null && Mathf.Approximately(globalFloat, EcAuthority(instance, i))) { DonorRuntime.Log.LogMessage((object)$"[WATER-FIX] global '{WaterGlobalNames[i]}' moved with live weather across the harvest window ({captured[i]:F3} -> {globalFloat:F3}, seen at {phase}) — legitimate drift, not touched."); continue; } if ((Object)(object)instance == (Object)null) { DonorRuntime.Log.LogWarning((object)$"[WATER-FIX] global '{WaterGlobalNames[i]}' changed across the harvest window ({captured[i]:F3} -> {globalFloat:F3}, seen at {phase}) but EnvironmentConditions is gone — cannot arbitrate, leaving it."); continue; } DonorRuntime.Log.LogWarning((object)($"[WATER-FIX] global '{WaterGlobalNames[i]}' was STOMPED across the harvest window ({captured[i]:F3} -> {globalFloat:F3}, EC authority {EcAuthority(instance, i):F3}, caught at {phase})." + (restore ? " Resetting the live EC's write caches so its next Update re-asserts all three (the game's own re-assert mechanism). An Awake-path shader stomper exists; the freeze only blocks Start/Update." : " Observed only at this phase."))); if (restore) { flag = true; } } else if (restore) { Shader.SetGlobalFloat(WaterGlobalNames[i], captured[i]); DonorRuntime.Log.LogWarning((object)$"[WATER-FIX] global '{WaterGlobalNames[i]}' changed across the harvest window ({captured[i]:F3} -> {globalFloat:F3}) — restored the pre-harvest value (caught at {phase}). An Awake-path shader stomper exists; the freeze only blocks Start/Update."); } else { DonorRuntime.Log.LogMessage((object)$"[WATER-FIX] global '{WaterGlobalNames[i]}' drifted since the harvest ({captured[i]:F3} -> {globalFloat:F3}, seen at {phase}) — NOT restoring (live weather owns it now)."); } } if (flag) { instance.m_lastWetness = -1f; instance.m_lastWaterLevel = -1f; instance.m_lastRain = -1f; } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[WATER-FIX] water-globals check failed at " + phase + ": " + ex.Message)); } } internal static void RepairDisplayChain(string phase) { try { Camera val = default(Camera); CameraQuality val2 = default(CameraQuality); string text = default(string); if (!SkySnapshot.TryGetDisplayDesync(ref val, ref val2, ref text)) { return; } if ((Object)(object)val2 == (Object)null) { DonorRuntime.Log.LogWarning((object)("[DISPLAY-FIX] " + text + " (caught at " + phase + ") — but the main camera has no CameraQuality, cannot repair.")); return; } DonorRuntime.Log.LogWarning((object)("[DISPLAY-FIX] " + text + " (caught at " + phase + ") — rebuilding via CameraQuality.ProcessRenderToTexture() (black-screen fix).")); Camera main = Camera.main; RenderTexture val3 = (RenderTexture)(((Object)(object)GameDisplayInUI.Instance != (Object)null && GameDisplayInUI.Instance.Screens.Length != 0 && (Object)(object)GameDisplayInUI.Instance.Screens[0] != (Object)null) ? /*isinst with value type is only supported in some contexts*/: null); RenderTexture val4 = (((Object)(object)main != (Object)null) ? main.targetTexture : null); val2.ProcessRenderToTexture(); RenderTexture val5 = (((Object)(object)main != (Object)null) ? main.targetTexture : null); int num = 0; if ((Object)(object)val3 != (Object)null && val3 != val5) { val3.Release(); Object.Destroy((Object)(object)val3); num++; } if ((Object)(object)val4 != (Object)null && val4 != val5 && val4 != val3) { val4.Release(); Object.Destroy((Object)(object)val4); num++; } CameraQuality val6 = default(CameraQuality); string arg = default(string); SkySnapshot.TryGetDisplayDesync(ref val, ref val6, ref arg); DonorRuntime.Log.LogMessage((object)$"[DISPLAY-FIX] after repair: {arg} (released {num} orphaned RT(s))."); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[DISPLAY-FIX] repair failed at " + phase + ": " + ex.Message)); } } } public static class DonorHarvest { private const float LoadHangWarnSeconds = 30f; private const float LoadHangAbandonSeconds = 120f; public static int CyclesThisSession { get; private set; } public static Dictionary> DonorScenes => DonorSceneTable.DonorScenes; public static Dictionary DonorPins => DonorSceneTable.DonorPins; public static string PinFor(string tableKey) { return DonorSceneTable.PinFor(tableKey); } public static string IdentityFor(string tableKey, Character donor) { return DonorSceneTable.IdentityFor(tableKey, donor); } public static bool TryGetDonorScenes(string speciesId, out List sceneNames, out string searchTerm) { return DonorSceneTable.TryGetDonorScenes(speciesId, out sceneNames, out searchTerm); } public static bool TryGetExpeditionScenes(string speciesId, out List sceneNames, out string searchTerm) { return DonorSceneTable.TryGetExpeditionScenes(speciesId, out sceneNames, out searchTerm); } public static string ResolveBuildScene(string want, out List similar) { return DonorSceneTable.ResolveBuildScene(want, out similar); } public static void DumpBuildScenes() { DonorSceneTable.DumpBuildScenes(); } public static Character FindLiveInScene(Scene scene, string creatureName) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return DonorCreatureFinder.FindLiveInScene(scene, creatureName); } public static Character FindLiveInScene(Scene scene, string creatureName, out int rank) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return DonorCreatureFinder.FindLiveInScene(scene, creatureName, out rank); } public static bool RemoveFromAudioRegistries(AmbienceSound a) { return DonorAudioRegistry.RemoveFromAudioRegistries(a); } public static void PruneDeadSoundPlayers() { DonorAudioRegistry.PruneDeadSoundPlayers(); } public static void AuditAudioRegistries(string context) { DonorAudioRegistry.AuditAudioRegistries(context); } public static IEnumerator HarvestChain(List sceneNames, string creatureName, Func use, Action onResult) { object result = null; string fallbackScene = null; for (int i = 0; i < sceneNames.Count; i++) { if (result != null) { break; } if (sceneNames.Count > 1) { DonorRuntime.Log.LogMessage((object)$"[HARVEST] chain {i + 1}/{sceneNames.Count} for '{creatureName}': '{sceneNames[i]}'."); } bool acceptSubstring = DonorChain.AcceptSubstringAt(i, sceneNames.Count, fallbackScene != null); object r = null; int rank = 0; yield return HarvestRanked(sceneNames[i], creatureName, use, acceptSubstring, delegate(object res, int rk) { r = res; rank = rk; }); if (r != null) { result = r; break; } if (rank == 1 && fallbackScene == null) { fallbackScene = sceneNames[i]; } } if (result == null && fallbackScene != null) { DonorRuntime.Log.LogMessage((object)("[HARVEST] no exact-name '" + creatureName + "' donor in the chain — falling back to the substring donor in '" + fallbackScene + "'.")); object r2 = null; yield return HarvestRanked(fallbackScene, creatureName, use, acceptSubstring: true, delegate(object res, int rk) { r2 = res; }); result = r2; } if (result == null && sceneNames.Count > 1) { DonorRuntime.Log.LogWarning((object)("[HARVEST] chain exhausted — no candidate yielded a live '" + creatureName + "'.")); } onResult(result); } private static IEnumerator HarvestRanked(string sceneName, string creatureName, Func use, bool acceptSubstring, Action onResult) { int foundRank = 0; yield return HarvestScene(sceneName, "creature='" + creatureName + "'", delegate(Scene donor) { //IL_0000: 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_0136: 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) int rank; Character val = DonorCreatureFinder.FindLiveInScene(donor, creatureName, out rank); foundRank = rank; if ((Object)(object)val == (Object)null) { DonorRuntime.Log.LogWarning((object)("[HARVEST] no live '" + creatureName + "' found among donor scene roots.")); DonorCreatureFinder.DumpSceneContents(donor); return (object)null; } if (rank < 2 && !acceptSubstring) { DonorRuntime.Log.LogMessage((object)("[HARVEST] '" + sceneName + "' has only a substring donor '" + val.Name + "' for '" + creatureName + "' — deferring in case a later scene holds an exact-name '" + creatureName + "'.")); return (object)null; } if (rank < 2) { DonorRuntime.Log.LogMessage((object)("[HARVEST] no exact-name donor for '" + creatureName + "' — using nearest substring match '" + val.Name + "' in '" + sceneName + "'.")); } ManualLogSource log = DonorRuntime.Log; string[] obj = new string[5] { "[HARVEST] found donor '", val.Name, "' at ", null, null }; Vector3 position = ((Component)val).transform.position; obj[3] = ((Vector3)(ref position)).ToString("F1"); obj[4] = " — running payload."; log.LogMessage((object)string.Concat(obj)); return use(val); }, delegate(object r) { onResult.Invoke(r, foundRank); }); } public static IEnumerator Harvest(string sceneName, string creatureName, Func use, Action onResult) { return HarvestScene(sceneName, "creature='" + creatureName + "'", delegate(Scene donor) { //IL_0000: 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_0035: Unknown result type (might be due to invalid IL or missing references) Character val = DonorCreatureFinder.FindLiveInScene(donor, creatureName); if ((Object)(object)val == (Object)null) { DonorRuntime.Log.LogWarning((object)("[HARVEST] no live '" + creatureName + "' found among donor scene roots.")); DonorCreatureFinder.DumpSceneContents(donor); return (object)null; } ManualLogSource log = DonorRuntime.Log; string[] obj = new string[5] { "[HARVEST] found donor '", val.Name, "' at ", null, null }; Vector3 position = ((Component)val).transform.position; obj[3] = ((Vector3)(ref position)).ToString("F1"); obj[4] = " — running payload."; log.LogMessage((object)string.Concat(obj)); return use(val); }, onResult); } public static IEnumerator HarvestScene(string sceneName, string label, Func useScene, Action onResult) { DonorRuntime.Log.LogMessage((object)("[HARVEST] start: scene='" + sceneName + "' " + label)); float t0 = Time.realtimeSinceStartup; Scene activeScene = SceneManager.GetActiveScene(); if (((Scene)(ref activeScene)).name == sceneName) { DonorRuntime.Log.LogWarning((object)("[HARVEST] donor scene '" + sceneName + "' IS the current scene — refusing (re-form from a wild one instead).")); onResult(null); yield break; } if (DonorPhotonGuard.WindowActive) { DonorRuntime.Log.LogMessage((object)("[HARVEST] another harvest's guard window is open — '" + sceneName + "' waiting its turn (harvests are serialized).")); while (DonorPhotonGuard.WindowActive) { yield return null; } DonorRuntime.Log.LogMessage((object)$"[HARVEST] '{sceneName}' proceeding after {Time.realtimeSinceStartup - t0:F2}s wait."); } int guardToken = DonorPhotonGuard.Begin(sceneName); GlobalAudioManager realAudio = Global.AudioManager; GlobalCombatManager realCombat = Global.CombatManager; EnvironmentConditions realEnv = EnvironmentConditions.Instance; AISquadManager realSquads = AISquadManager.Instance; TerrainMeshQualityManager realTerrainMesh = TerrainMeshQualityManager.Instance; Material realSkybox = RenderSettings.skybox; SkySnapshot.Log("pre-harvest '" + sceneName + "'", DonorRuntime.Log); float[] realWaterGlobals = DonorDisplayRepair.CaptureWaterGlobals(); int frozenAtLoad = 0; UnityAction freezeOnLoad = delegate(Scene s, LoadSceneMode m) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 if ((int)m == 1 && !(((Scene)(ref s)).name != sceneName)) { GameObject[] rootGameObjects4 = ((Scene)(ref s)).GetRootGameObjects(); foreach (GameObject val6 in rootGameObjects4) { DonorDisplayRepair.NeutralizeCamerasUnder(val6, "sceneLoaded"); val6.SetActive(false); frozenAtLoad++; } } }; SceneManager.sceneLoaded += freezeOnLoad; DonorDisplayRepair.ArmCameraSentinel(sceneName); bool restoresRan = false; object result = null; AssetBundle meshBundleShield = null; try { AsyncOperation load; try { load = SceneManager.LoadSceneAsync(sceneName, (LoadSceneMode)1); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[HARVEST] LoadSceneAsync threw: " + ex.Message)); onResult(null); yield break; } if (load == null) { DonorRuntime.Log.LogWarning((object)("[HARVEST] LoadSceneAsync returned null — bad scene name '" + sceneName + "'?")); onResult(null); yield break; } float hangWarnAt = Time.realtimeSinceStartup + 30f; float abandonAt = Time.realtimeSinceStartup + 30f + 120f; while (!load.isDone) { DonorPhotonGuard.Refresh(guardToken); if (hangWarnAt > 0f && Time.realtimeSinceStartup >= hangWarnAt) { DonorRuntime.Log.LogWarning((object)$"[HARVEST] donor scene '{sceneName}' load still not done after {30f:F0}s — possible hang (check output_log.txt for Unity-side load errors)."); hangWarnAt = -1f; } if (Time.realtimeSinceStartup >= abandonAt) { DonorRuntime.Log.LogWarning((object)$"[HARVEST] ABANDONING donor scene '{sceneName}' — load STILL not done after {150f:F0}s (truly wedged). Closing the guard window + restoring singletons so saves/harvests aren't starved for the rest of the session; the scene is left loading."); onResult(null); yield break; } yield return null; } DonorPhotonGuard.Refresh(guardToken); RunSingletonRestores(); DonorDisplayRepair.RestoreSkybox(realSkybox, "load-done"); Scene sceneByName = SceneManager.GetSceneByName(sceneName); if (!((Scene)(ref sceneByName)).IsValid()) { DonorRuntime.Log.LogWarning((object)("[HARVEST] donor scene '" + sceneName + "' not valid after load.")); onResult(null); yield break; } DonorRuntime.Log.LogMessage((object)$"[HARVEST] donor scene loaded in {Time.realtimeSinceStartup - t0:F2}s ({((Scene)(ref sceneByName)).rootCount} roots)."); int num = 0; GameObject[] rootGameObjects = ((Scene)(ref sceneByName)).GetRootGameObjects(); foreach (GameObject val in rootGameObjects) { num += val.GetComponentsInChildren(true).Length; } DonorRuntime.Log.LogMessage((object)$"[PHOTON-GUARD] donor scene carries {num} PhotonView(s)."); GameObject[] rootGameObjects2 = ((Scene)(ref sceneByName)).GetRootGameObjects(); foreach (GameObject val2 in rootGameObjects2) { Camera[] componentsInChildren = val2.GetComponentsInChildren(true); foreach (Camera val3 in componentsInChildren) { DonorRuntime.Log.LogMessage((object)$"[DISPLAY-FIX] donor scene carries a Camera '{((Object)val3).name}' (active={((Component)val3).gameObject.activeInHierarchy}, CameraQuality={(Object)(object)((Component)val3).GetComponent() != (Object)null}) — potential display-chain clobberer."); } } DonorDisplayRepair.NeutralizeDonorScene(sceneByName, "load-done"); try { DonorAudioRegistry.DetachAmbience(sceneByName); GameObject[] rootGameObjects3 = ((Scene)(ref sceneByName)).GetRootGameObjects(); foreach (GameObject val4 in rootGameObjects3) { val4.SetActive(false); } result = useScene(sceneByName); } catch (Exception ex2) { DonorRuntime.Log.LogWarning((object)("[HARVEST] processing threw: " + ex2.Message + "\n" + ex2.StackTrace)); } float u0 = Time.realtimeSinceStartup; DonorPhotonGuard.Refresh(guardToken); meshBundleShield = TerrainGuard.BeginMeshBundleShield(sceneByName, "pre-unload"); AsyncOperation val5 = SceneManager.UnloadSceneAsync(sceneByName); if (val5 != null) { yield return val5; } TerrainGuard.EndMeshBundleShield(meshBundleShield, "post-unload"); meshBundleShield = null; CyclesThisSession++; DonorRuntime.Log.LogMessage((object)$"[HARVEST] donor scene unloaded in {Time.realtimeSinceStartup - u0:F2}s (session donor cycles={CyclesThisSession})."); float realtimeSinceStartup = Time.realtimeSinceStartup; try { LightProbes.Tetrahedralize(); DonorRuntime.Log.LogMessage((object)$"[PROBE-FIX] light probes re-tetrahedralized in {Time.realtimeSinceStartup - realtimeSinceStartup:F2}s (LightProbesManager crash mitigation)."); } catch (Exception ex3) { DonorRuntime.Log.LogWarning((object)("[PROBE-FIX] Tetrahedralize failed: " + ex3.Message)); } DonorDisplayRepair.RestoreSkybox(realSkybox, "post-unload"); DonorDisplayRepair.RepairDisplayChain("post-unload"); DonorDisplayRepair.CheckWaterGlobals(realWaterGlobals, "post-unload", restore: true); SkySnapshot.Log("post-unload", DonorRuntime.Log); if ((Object)(object)DonorRuntime.Runner != (Object)null) { DonorRuntime.Runner.StartCoroutine(DonorDisplayRepair.DeferredSkyCheck(realSkybox, realWaterGlobals)); } } finally { SceneManager.sceneLoaded -= freezeOnLoad; DonorDisplayRepair.DisarmCameraSentinel(); TerrainGuard.EndMeshBundleShield(meshBundleShield, "finally"); RunSingletonRestores(); DonorPhotonGuard.End(guardToken); } if (frozenAtLoad > 0) { DonorRuntime.Log.LogMessage((object)$"[HARVEST] froze {frozenAtLoad} donor root(s) at sceneLoaded — donor Start/Update never ran (global-state protection)."); } else { DonorRuntime.Log.LogWarning((object)"[HARVEST] sceneLoaded freeze did NOT fire — donor scripts may have run Start/Update (watch for [DISPLAY-FIX]/global-state symptoms)."); } DonorAudioRegistry.PruneDeadSoundPlayers(); SoulSpotGuard.PruneDead(); DonorAudioRegistry.AuditAudioRegistries("post-harvest"); DonorRuntime.Log.LogMessage((object)string.Format("[HARVEST] done in {0:F2}s total — result={1}.", Time.realtimeSinceStartup - t0, (result != null) ? "OK" : "FAILED")); onResult(result); void RunSingletonRestores() { if (!restoresRan) { restoresRan = true; DonorSingletonRestore.RestoreAudioManager(realAudio); DonorSingletonRestore.RestoreCombatManager(realCombat); DonorSingletonRestore.RestoreEnvironment(realEnv); DonorSingletonRestore.RestoreSquadManager(realSquads); DonorSingletonRestore.RestoreTerrainMeshManager(realTerrainMesh); } } } } [HarmonyPatch(typeof(CharacterAI), "NetworkInit")] public static class DonorNetworkInitGuard { [HarmonyPrefix] private static bool Prefix(CharacterAI __instance) { return OnNetworkInit(__instance); } internal static bool OnNetworkInit(CharacterAI ai) { try { if (!PhotonNetwork.isNonMasterClientInRoom) { return true; } if ((Object)(object)ai == (Object)null || !DonorPhotonGuard.IsDonorSceneObject(((Component)ai).gameObject)) { return true; } string activeDonorScene = DonorPhotonGuard.ActiveDonorScene; DonorRuntime.Log.LogMessage((object)("[HARVEST-GUARD] suppressed guest NetworkInit replica-conversion: '" + ((Object)ai).name + "' in donor scene '" + activeDonorScene + "' — donor keeps CharacterAI, the template stays master-shaped.")); return false; } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[HARVEST-GUARD] prefix error (vanilla NetworkInit allowed): " + ex.Message)); return true; } } } [HarmonyPatch(typeof(NetworkingPeer), "RegisterPhotonView")] public static class RegisterPhotonViewGuard { [HarmonyPrefix] private static bool Prefix(NetworkingPeer __instance, PhotonView netView) { return DonorPhotonGuard.OnRegister(__instance, netView); } } public static class DonorPhotonGuard { private const float ExpirySeconds = 30f; private static readonly GuardWindow _window = new GuardWindow(30.0); private static string _scene; private static int _skipped; private static bool Active => _window.Active((double)Time.realtimeSinceStartup); public static bool WindowActive => Active; public static string ActiveDonorScene { get { if (!Active) { return null; } return _scene; } } public static bool IsDonorSceneObject(GameObject go) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)go == (Object)null) { return false; } string activeDonorScene = ActiveDonorScene; if (activeDonorScene != null) { Scene scene = go.scene; return ((Scene)(ref scene)).name == activeDonorScene; } return false; } public static int Begin(string donorScene) { float realtimeSinceStartup = Time.realtimeSinceStartup; if (_window.Owner != 0) { DonorRuntime.Log.LogWarning((object)("[PHOTON-GUARD] window for '" + _scene + "' still open at Begin — " + (_window.Expired((double)realtimeSinceStartup) ? "EXPIRED (wedged harvest; taking over — its late End will be ignored)." : "NOT expired: harvest serialization broke, overlapping windows!"))); } int num = _window.Begin((double)realtimeSinceStartup); _scene = donorScene; _skipped = 0; DonorRuntime.Log.LogMessage((object)$"[PHOTON-GUARD] window OPEN for donor scene '{donorScene}' (token {num})."); return num; } public static void Refresh(int token) { _window.Refresh(token, (double)Time.realtimeSinceStartup); } public static void End(int token) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Invalid comparison between Unknown and I4 //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 GuardEndResult val = _window.End(token); if ((int)val != 1) { if ((int)val == 2) { DonorRuntime.Log.LogWarning((object)$"[PHOTON-GUARD] stale End (token {token}) ignored — the window is now owned by token {_window.Owner} for '{_scene}'."); return; } string text = (((double)Time.realtimeSinceStartup >= _window.ExpireAt) ? " (deadline LAPSED before close — the tail of this harvest ran unguarded; heartbeat gap?)" : ""); int num = ((PhotonNetwork.networkingPeer != null && PhotonNetwork.networkingPeer.photonViewList != null) ? PhotonNetwork.networkingPeer.photonViewList.Count : (-1)); DonorRuntime.Log.LogMessage((object)$"[PHOTON-GUARD] window CLOSED for '{_scene}'{text}: suppressed {_skipped} donor view registration(s); registry holds {num} view(s)."); _scene = null; _skipped = 0; } } public static bool OnRegister(NetworkingPeer peer, PhotonView netView) { //IL_00d2: 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_0122: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)netView == (Object)null) { return true; } if (IsDonorSceneObject(((Component)netView).gameObject)) { netView.removedFromLocalViewList = true; _skipped++; DonorRuntime.Log.LogMessage((object)$"[PHOTON-GUARD] suppressed donor view: id={netView.viewID} go='{((Object)((Component)netView).gameObject).name}'."); return false; } if (peer != null && peer.photonViewList != null && peer.photonViewList.TryGetValue(netView.viewID, out var value) && (Object)(object)value != (Object)(object)netView) { object obj; Scene scene; if (!((Object)(object)value != (Object)null)) { obj = ""; } else { string[] obj2 = new string[5] { "'", ((Object)((Component)value).gameObject).name, "' (scene '", null, null }; scene = ((Component)value).gameObject.scene; obj2[3] = ((Scene)(ref scene)).name; obj2[4] = "')"; obj = string.Concat(obj2); } string text = (string)obj; ManualLogSource log = DonorRuntime.Log; object[] obj3 = new object[4] { netView.viewID, ((Object)((Component)netView).gameObject).name, null, null }; scene = ((Component)netView).gameObject.scene; obj3[2] = ((Scene)(ref scene)).name; obj3[3] = text; log.LogMessage((object)string.Format("[PHOTON-RECON] duplicate view id {0}: new='{1}' (scene '{2}') will destroy old={3}.", obj3)); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[PHOTON-GUARD] prefix error (registration allowed): " + ex.Message)); } return true; } public static void DumpRegistry() { //IL_0227: Unknown result type (might be due to invalid IL or missing references) //IL_022c: Unknown result type (might be due to invalid IL or missing references) NetworkingPeer peer = PhotonNetwork.networkingPeer; if (peer == null || peer.photonViewList == null) { DonorRuntime.Log.LogMessage((object)"[PHOTON-RECON] no networking peer / view list."); return; } int num = peer.photonViewList.Values.Count((PhotonView v) => (Object)(object)v == (Object)null); DonorRuntime.Log.LogMessage((object)$"[PHOTON-RECON] registry: {peer.photonViewList.Count} view(s), {num} destroyed-but-still-listed."); try { List manuallyAllocatedViewIds = PhotonNetwork.manuallyAllocatedViewIds; if (manuallyAllocatedViewIds == null) { DonorRuntime.Log.LogMessage((object)"[PHOTON-RECON] manuallyAllocatedViewIds: (Photon not initialized)."); } else { List list = manuallyAllocatedViewIds.OrderBy((int i) => i).ToList(); int num2 = list.Count((int i) => !peer.photonViewList.ContainsKey(i)); string arg = ((list.Count == 0) ? "" : (": " + string.Join(", ", list.Take(60)) + ((list.Count > 60) ? $" … (+{list.Count - 60} more)" : ""))); DonorRuntime.Log.LogMessage((object)($"[PHOTON-RECON] manuallyAllocatedViewIds: {list.Count} id(s), " + $"{num2} with no registered view (leak candidates / deferred corpse releases){arg}")); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[PHOTON-RECON] manuallyAllocatedViewIds read failed: " + ex.Message)); } foreach (KeyValuePair item in peer.photonViewList.OrderBy((KeyValuePair k) => k.Key).Take(40)) { object obj; if (!((Object)(object)item.Value != (Object)null)) { obj = ""; } else { string name = ((Object)((Component)item.Value).gameObject).name; Scene scene = ((Component)item.Value).gameObject.scene; obj = $"go='{name}' scene='{((Scene)(ref scene)).name}' active={((Component)item.Value).gameObject.activeInHierarchy}"; } string arg2 = (string)obj; DonorRuntime.Log.LogMessage((object)$"[PHOTON-RECON] id={item.Key} {arg2}"); } if (peer.photonViewList.Count > 40) { DonorRuntime.Log.LogMessage((object)$"[PHOTON-RECON] … {peer.photonViewList.Count - 40} more (photondump shows the low/baked id range)."); } } } public static class DonorRuntime { public static ManualLogSource Log => Plugin.Log; public static MonoBehaviour Runner => (MonoBehaviour)(object)Plugin.Instance; public static bool IsSanePosition(Vector3 p) { //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) //IL_000c: Unknown result type (might be due to invalid IL or missing references) return WorldPosition.IsSane(p.x, p.y, p.z); } } [HarmonyPatch(typeof(SaveManager), "Save", new Type[] { typeof(bool), typeof(bool) })] public static class DonorSaveGuard { [HarmonyPrefix] private static bool Prefix(SaveManager __instance, bool _async, bool _forceSaveEnvironment) { try { if (!DonorPhotonGuard.WindowActive) { return true; } __instance.SetSaveRequired(); DonorRuntime.Log.LogWarning((object)$"[SAVE-GUARD] save requested mid-harvest (async={_async}, forceEnv={_forceSaveEnvironment}) — deferred via SetSaveRequired; vanilla re-fires it after the window closes."); return false; } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[SAVE-GUARD] prefix error (save allowed): " + ex.Message)); return true; } } } internal static class DonorSceneTable { private static Dictionary> _donorScenes; private static Dictionary _donorPins; internal static Dictionary> DonorScenes { get { if (_donorScenes == null) { LoadDonorScenes(); } return _donorScenes; } } internal static Dictionary DonorPins { get { if (_donorScenes == null) { LoadDonorScenes(); } return _donorPins; } } internal static string PinFor(string tableKey) { if (string.IsNullOrEmpty(tableKey)) { return null; } if (!DonorPins.TryGetValue(tableKey.Trim(), out var value)) { return null; } return value; } internal static string IdentityFor(string tableKey, Character donor) { string result = (((Object)(object)donor != (Object)null) ? donor.Name : null); if (string.IsNullOrEmpty(tableKey)) { return result; } if (PinFor(tableKey) == null) { return result; } return tableKey.Trim(); } private static void LoadDonorScenes() { string text = ReadEmbeddedDonorScenes(); Dictionary> dictionary = DonorTable.Parse(text); Dictionary dictionary2 = DonorTable.ParsePins(text); DonorRuntime.Log.LogMessage((object)(string.Format("[HARVEST] loaded {0} built-in donor species entr{1}", dictionary.Count, (dictionary.Count == 1) ? "y" : "ies") + ((dictionary2.Count > 0) ? $", {dictionary2.Count} of them pinned to a donor GameObject name" : "") + ".")); try { string path = Path.Combine(Paths.ConfigPath, "DonorScenes.txt"); if (File.Exists(path)) { string text2 = File.ReadAllText(path); Dictionary> dictionary3 = DonorTable.Parse(text2); dictionary = DonorTable.Merge(dictionary, dictionary3); dictionary2 = DonorTable.MergePins(dictionary2, DonorTable.ParsePins(text2)); DonorRuntime.Log.LogMessage((object)string.Format("[HARVEST] merged {0} donor species entr{1} from config override (tried first).", dictionary3.Count, (dictionary3.Count == 1) ? "y" : "ies")); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[HARVEST] DonorScenes.txt override load failed: " + ex.Message)); } _donorScenes = dictionary; _donorPins = dictionary2; } internal static bool RegisterSpecies(string speciesKey, IEnumerable donorScenes) { string text = speciesKey?.Trim() ?? ""; List list = new List(); if (donorScenes != null) { foreach (string donorScene in donorScenes) { if (!string.IsNullOrEmpty(donorScene?.Trim())) { list.Add(donorScene.Trim()); } } } if (text.Length == 0 || list.Count == 0) { DonorRuntime.Log.LogWarning((object)"[HARVEST] RegisterSpecies: a species key and at least one donor scene are required."); return false; } Dictionary> donorScenes2 = DonorScenes; if (!donorScenes2.TryGetValue(text, out var value) || value == null) { value = (donorScenes2[text] = new List()); } int num = 0; foreach (string s in list) { if (value.FindIndex((string x) => string.Equals(x, s, StringComparison.OrdinalIgnoreCase)) < 0) { value.Add(s); num++; } } DonorRuntime.Log.LogMessage((object)($"[HARVEST] registered species '{text}' at runtime: {num} donor scene(s) added " + $"(chain now {value.Count}) — it is now harvestable/spawnable like any DonorScenes.txt row.")); return true; } private static string ReadEmbeddedDonorScenes() { Assembly executingAssembly = Assembly.GetExecutingAssembly(); string text = Array.Find(executingAssembly.GetManifestResourceNames(), (string n) => n.EndsWith("DonorScenes.txt", StringComparison.OrdinalIgnoreCase)); if (text == null) { DonorRuntime.Log.LogWarning((object)"[HARVEST] embedded DonorScenes.txt not found."); return ""; } using Stream stream = executingAssembly.GetManifestResourceStream(text); using StreamReader streamReader = new StreamReader(stream); return streamReader.ReadToEnd(); } internal static bool TryGetDonorScenes(string speciesId, out List sceneNames, out string searchTerm) { //IL_00b8: 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) sceneNames = null; List list = default(List); if (!SpeciesTable.TryResolveKey>(DonorScenes, speciesId, ref searchTerm, ref list, (string)null) || list == null || list.Count == 0) { return false; } List list3 = default(List); List list2 = DonorTable.FilterViable((IEnumerable)list, ref list3); if (list3.Count > 0) { DonorRuntime.Log.LogWarning((object)string.Format("[HARVEST] '{0}': skipped {1} OVERSIZED donor candidate(s) ({2}) — region terrains/towns are never loaded as donors (global-state stomp + crash risk; find a dungeon donor via donorspot).", searchTerm, list3.Count, string.Join(", ", list3))); } if (list2.Count == 0) { DonorRuntime.Log.LogWarning((object)("[HARVEST] '" + searchTerm + "' has NO additive donor — every candidate is an oversized region/town scene (" + string.Join(", ", list3) + "). Reach it via an EXPEDITION instead: run 'expedition " + searchTerm + "' (or add a dungeon donor to its manifest).")); return false; } Scene activeScene = SceneManager.GetActiveScene(); sceneNames = DonorTable.OrderCandidates((IEnumerable)list2, ((Scene)(ref activeScene)).name); return true; } internal static bool TryGetExpeditionScenes(string speciesId, out List sceneNames, out string searchTerm) { sceneNames = null; List list = default(List); if (!SpeciesTable.TryResolveKey>(DonorScenes, speciesId, ref searchTerm, ref list, (string)null) || list == null || list.Count == 0) { return false; } List list2 = DonorTable.ExpeditionCandidates((IEnumerable)list); if (list2.Count == 0) { return false; } sceneNames = list2; return true; } internal static string ResolveBuildScene(string want, out List similar) { return BuildScenes.Resolve(want, ref similar); } internal static void DumpBuildScenes() { BuildScenes.Dump(DonorRuntime.Log); } } internal static class DonorSingletonRestore { internal static void RestoreAudioManager(GlobalAudioManager original) { //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) GlobalAudioManager audioManager = Global.AudioManager; if ((Object)(object)original == (Object)null) { if ((Object)(object)audioManager != (Object)null) { DonorRuntime.Log.LogWarning((object)"[AMBIENCE-FIX] no audio manager existed pre-load but one does now — leaving it in place."); } } else if (!((Object)(object)audioManager == (Object)(object)original)) { Global.AudioManager = original; ManualLogSource log = DonorRuntime.Log; object obj; if (!((Object)(object)audioManager == (Object)null)) { Scene scene = ((Component)audioManager).gameObject.scene; obj = "'" + ((Scene)(ref scene)).name + "'s"; } else { obj = "already destroyed"; } log.LogMessage((object)("[AMBIENCE-FIX] donor GlobalAudioManager hijacked the singleton (found " + (string?)obj + ") — restored the real one (bug-2 fix).")); } } internal static void RestoreCombatManager(GlobalCombatManager original) { //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) GlobalCombatManager combatManager = Global.CombatManager; if ((Object)(object)original == (Object)null) { if ((Object)(object)combatManager != (Object)null) { DonorRuntime.Log.LogWarning((object)"[COMBAT-FIX] no combat manager existed pre-load but one does now — leaving it in place."); } } else if (!((Object)(object)combatManager == (Object)(object)original)) { Global.CombatManager = original; ManualLogSource log = DonorRuntime.Log; object obj; if (!((Object)(object)combatManager == (Object)null)) { Scene scene = ((Component)combatManager).gameObject.scene; obj = "'" + ((Scene)(ref scene)).name + "'s"; } else { obj = "already destroyed"; } log.LogMessage((object)("[COMBAT-FIX] donor GlobalCombatManager hijacked the singleton (found " + (string?)obj + ") — restored the real one (bug-12 fix).")); } } internal static void RestoreEnvironment(EnvironmentConditions original) { //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_009e: 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_0053: Unknown result type (might be due to invalid IL or missing references) EnvironmentConditions instance = EnvironmentConditions.Instance; Scene scene; if ((Object)(object)original == (Object)null) { if ((Object)(object)instance != (Object)null) { DonorRuntime.Log.LogWarning((object)"[ENV-FIX] no EnvironmentConditions existed pre-load but one does now — leaving it in place."); return; } EnvironmentConditions val = Object.FindObjectOfType(); if (!((Object)(object)val == (Object)null)) { EnvironmentConditions.Instance = val; ManualLogSource log = DonorRuntime.Log; scene = ((Component)val).gameObject.scene; log.LogWarning((object)("[ENV-FIX] EnvironmentConditions.Instance was ALREADY dangling before this harvest — adopted the live '" + ((Scene)(ref scene)).name + "' instance (compounded bug-12).")); } } else if (!((Object)(object)instance == (Object)(object)original)) { EnvironmentConditions.Instance = original; ManualLogSource log2 = DonorRuntime.Log; object obj; if (!((Object)(object)instance == (Object)null)) { scene = ((Component)instance).gameObject.scene; obj = "'" + ((Scene)(ref scene)).name + "'s"; } else { obj = "already destroyed"; } log2.LogMessage((object)("[ENV-FIX] donor EnvironmentConditions hijacked the singleton (found " + (string?)obj + ") — restored the real one (bug-12 true root cause).")); } } internal static void RestoreSquadManager(AISquadManager original) { //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) AISquadManager instance = AISquadManager.Instance; if ((Object)(object)original == (Object)null) { if ((Object)(object)instance != (Object)null) { DonorRuntime.Log.LogWarning((object)"[SQUAD-FIX] no AISquadManager existed pre-load but one does now — leaving it in place."); } } else if (!((Object)(object)instance == (Object)(object)original)) { AISquadManager.Instance = original; ManualLogSource log = DonorRuntime.Log; object obj; if (!((Object)(object)instance == (Object)null)) { Scene scene = ((Component)instance).gameObject.scene; obj = "'" + ((Scene)(ref scene)).name + "'s"; } else { obj = "already destroyed"; } log.LogMessage((object)("[SQUAD-FIX] donor AISquadManager hijacked the singleton (found " + (string?)obj + ") — restored the real one (bug-16 fix).")); } } internal static void RestoreTerrainMeshManager(TerrainMeshQualityManager original) { //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) TerrainMeshQualityManager instance = TerrainMeshQualityManager.Instance; if ((Object)(object)original == (Object)null) { if ((Object)(object)instance != (Object)null) { DonorRuntime.Log.LogWarning((object)"[TMESH-FIX] no live TerrainMeshQualityManager to restore (none existed pre-load, or the captured one has since been destroyed) but one exists now — leaving it in place."); } } else if (!((Object)(object)instance == (Object)(object)original)) { TerrainMeshQualityManager.Instance = original; ManualLogSource log = DonorRuntime.Log; object obj; if (!((Object)(object)instance == (Object)null)) { Scene scene = ((Component)instance).gameObject.scene; obj = "'" + ((Scene)(ref scene)).name + "'s"; } else { obj = "already destroyed"; } log.LogMessage((object)("[TMESH-FIX] donor TerrainMeshQualityManager hijacked the singleton (found " + (string?)obj + ") — restored the real one (V32-b fix, part 1).")); } } } public static class DonorVerbs { public static void RegisterAll(CommandRegistry c) { c.Register("photondump", "Photon view-registry health (Bug 1).", (Action)delegate { DonorPhotonGuard.DumpRegistry(); }); c.Register("audiodump", "GlobalAudioManager registrant audit (Bug 2).", (Action)delegate { DonorHarvest.AuditAudioRegistries("manual audiodump"); }); c.Register("audioprune", "Drop destroyed SoundPlayers from the static registry + audit.", (Action)delegate { DonorHarvest.PruneDeadSoundPlayers(); DonorHarvest.AuditAudioRegistries("post-prune"); }); c.Register("terraindump", "Snapshot terrain health now ([TERRAIN] meshTiles/brokenMesh/lod2Missing; render-hole diagnosis).", (Action)delegate { TerrainGuard.Dump("manual terraindump"); }); c.Register("terrainfix", "Force blanked terrain tiles to the COARSE Lod2 mesh. WARNING: draws the ground at the wrong height (Bug 32) — a zone reload is the clean fix.", (Action)delegate { TerrainGuard.RepairNow("manual terrainfix"); }); } } public static class ExpeditionHarvest { private sealed class PlayerMark { public Character Ch; public string Uid; public Vector3 Pos; public Quaternion Rot; } private const float WatchdogSeconds = 300f; private static string _donorScene; private static string _homeScene; private static readonly List _marks = new List(); private static Func _payload; private static Action _onDone; private static object _result; private static int _leg; private static bool _payloadRan; private static int _gameplayDoneLeg; private static NetworkLevelLoader _loader; private static int _generation; private static bool _returnRequested; private static bool _restoring; private static int _returnRetries; private static bool _rescued; private static int _resumeCount; private static int _lastResumeFrame; private static string _endScene; private static string _endReason; public static float ReturnLegDelaySeconds = 2f; private const float OutboundGraceSeconds = 30f; internal const string RoomPropKey = "CK_EXPED"; private static bool _roomPropStamped; private static int _sceneDoneCount; private static int _gameplayDoneCount; private const int MaxReturnRetries = 2; private const float RescueGraceSeconds = 60f; public static bool InProgress { get; private set; } public static bool LastTripEndedHome { get; private set; } = true; public static float ReturnRetrySeconds { get { if (DkConfig.Expedition.ReturnRetrySeconds == null) { return 20f; } return Mathf.Max(0f, DkConfig.Expedition.ReturnRetrySeconds.Value); } } public static bool Begin(string donorScene, Func useScene, Action onDone) { //IL_0048: 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_007e: Unknown result type (might be due to invalid IL or missing references) //IL_031e: Unknown result type (might be due to invalid IL or missing references) //IL_0328: Expected O, but got Unknown //IL_0328: Unknown result type (might be due to invalid IL or missing references) //IL_0332: Expected O, but got Unknown //IL_0344: Unknown result type (might be due to invalid IL or missing references) //IL_034e: Expected O, but got Unknown //IL_034e: Unknown result type (might be due to invalid IL or missing references) //IL_0358: Expected O, but got Unknown //IL_036a: Unknown result type (might be due to invalid IL or missing references) //IL_0374: Expected O, but got Unknown //IL_0374: Unknown result type (might be due to invalid IL or missing references) //IL_037e: Expected O, but got Unknown //IL_0227: Unknown result type (might be due to invalid IL or missing references) //IL_023e: 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_0250: Unknown result type (might be due to invalid IL or missing references) //IL_0255: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrEmpty(donorScene) || useScene == null) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] Begin: bad arguments."); return false; } if (InProgress) { DonorRuntime.Log.LogWarning((object)$"[EXPEDITION] already running ('{_donorScene}', leg {_leg}) — refusing a second one."); return false; } Scene activeScene = SceneManager.GetActiveScene(); if (string.Equals(((Scene)(ref activeScene)).name, donorScene, StringComparison.OrdinalIgnoreCase)) { DonorRuntime.Log.LogMessage((object)("[EXPEDITION] '" + donorScene + "' is the ACTIVE scene — running the payload in place (no loading screens).")); LastTripEndedHome = true; object obj = RunPayload(activeScene, useScene); try { onDone?.Invoke(obj); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] onDone threw: " + ex.Message)); } return true; } if (StorySense.IsPrologueActive()) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] refusing to depart the prologue/tutorial — a round trip here consumes one-shot story beats (finish the intro first)."); return false; } NetworkLevelLoader instance = NetworkLevelLoader.Instance; if ((Object)(object)instance == (Object)null) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] no NetworkLevelLoader yet."); return false; } if (instance.InLoading) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] a load is already in progress — try again once it settles."); return false; } if (DonorPhotonGuard.WindowActive) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] a donor-scene harvest window is open (an additive donor load is in flight — the vanilla loader does not see it) — refusing to depart under it. Try again in a few seconds."); return false; } if (PhotonNetwork.isNonMasterClientInRoom) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] only the HOST can run an expedition (it drives two area switches for the whole party) — ask the host."); return false; } int num = 0; try { PhotonPlayer[] otherPlayers = PhotonNetwork.otherPlayers; num = ((otherPlayers != null) ? otherPlayers.Length : 0); } catch (Exception ex2) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] otherPlayers read threw (" + ex2.GetType().Name + ") — co-op gate is judging as solo.")); } if (ExpeditionCoop.ShouldRefuseBegin(DkConfig.Expedition.AllowCoop.Value, PhotonNetwork.inRoom, num)) { DonorRuntime.Log.LogWarning((object)($"[EXPEDITION] refusing to start with {num} other player(s) connected — " + "a mid-trip guest gate wedge can strand the WHOLE party in the donor region and the watchdog cannot recover it. Set [Expedition] AllowCoop = true to allow co-op expeditions (testing only; every guest should run [Expedition] GuestAutoContinue = true).")); return false; } if ((Object)(object)DonorRuntime.Runner == (Object)null) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] no DonorKit plugin host for coroutines."); return false; } _marks.Clear(); CharacterManager instance2 = CharacterManager.Instance; if ((Object)(object)instance2 != (Object)null) { for (int i = 0; i < instance2.PlayerCharacters.Count; i++) { Character character = instance2.GetCharacter(instance2.PlayerCharacters.Values[i]); if (!((Object)(object)character == (Object)null)) { _marks.Add(new PlayerMark { Ch = character, Uid = UID.op_Implicit(character.UID), Pos = ((Component)character).transform.position, Rot = ((Component)character).transform.rotation }); } } } if (_marks.Count == 0) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] no player characters to mark — not starting."); return false; } _donorScene = donorScene; _homeScene = ((Scene)(ref activeScene)).name; _payload = useScene; _onDone = onDone; _result = null; _payloadRan = false; _gameplayDoneLeg = 0; _leg = 1; _returnRequested = false; _restoring = false; _returnRetries = 0; _rescued = false; _resumeCount = 0; _lastResumeFrame = 0; LastTripEndedHome = false; _loader = instance; _generation++; InProgress = true; try { NetworkLevelLoader loader = _loader; loader.onSceneLoadingDone = (UnityAction)Delegate.Combine((Delegate?)(object)loader.onSceneLoadingDone, (Delegate?)new UnityAction(HandleSceneLoadingDone)); NetworkLevelLoader loader2 = _loader; loader2.onGameplayLoadingDone = (UnityAction)Delegate.Combine((Delegate?)(object)loader2.onGameplayLoadingDone, (Delegate?)new UnityAction(HandleGameplayLoadingDone)); NetworkLevelLoader loader3 = _loader; loader3.OnResumeAfterLoading = (UnityAction)Delegate.Combine((Delegate?)(object)loader3.OnResumeAfterLoading, (Delegate?)new UnityAction(HandleResumeAfterLoading)); StampRoomProperty(); DonorRuntime.Runner.StartCoroutine(AutoContinue(_generation)); DonorRuntime.Runner.StartCoroutine(Watchdog(_generation)); DonorRuntime.Log.LogMessage((object)("[EXPEDITION] departing '" + _homeScene + "' → '" + _donorScene + "' " + $"({_marks.Count} player position(s) marked; the game saves on each leg, vanilla door behavior).")); _loader.RequestSwitchArea(_donorScene, 0, 1.5f, false); DonorRuntime.Runner.StartCoroutine(OutboundWatch(_generation)); return true; } catch (Exception arg) { DonorRuntime.Log.LogError((object)($"[EXPEDITION] failed to launch '{_donorScene}': {arg}. " + "Releasing the expedition guard — nobody left home, so nothing is stranded.")); _onDone = null; Finish("launch threw"); return false; } } private static IEnumerator OutboundWatch(int gen) { float t0 = Time.unscaledTime; while (Time.unscaledTime - t0 < 30f) { if (!InProgress || gen != _generation) { yield break; } yield return (object)new WaitForSeconds(0.5f); } if (InProgress && gen == _generation && _leg == 1 && _gameplayDoneLeg == 0 && !LoaderBusy() && IsActiveScene(_homeScene)) { DonorRuntime.Log.LogWarning((object)($"[EXPEDITION] the switch to '{_donorScene}' was requested {30f:F0}s ago " + "but no load ever started and we are still in '" + _homeScene + "' — the donor scene never loaded. Aborting (nothing was moved).")); Finish("the outbound to '" + _donorScene + "' never started"); } } public static string Status() { return (InProgress ? ($"expedition IN PROGRESS: '{_homeScene}' → '{_donorScene}' leg {_leg}/2, payload ran={_payloadRan}, " + $"gate phase reached for leg {_gameplayDoneLeg}, returnRequested={_returnRequested}, " + $"restoring={_restoring}, resumes={_resumeCount}, returnRetries={_returnRetries}") : $"no expedition in progress (last trip ended home: {LastTripEndedHome})") + $" (return-leg delay {ReturnLegDelaySeconds:F1}s, return retry {ReturnRetrySeconds:F0}s)"; } public static string ForceReset() { if (!InProgress) { return "[EXPEDITION] no expedition in progress — nothing to reset."; } string text = $"'{_homeScene}' → '{_donorScene}' leg {_leg}/2"; _result = null; Finish("force reset (expeditionreset verb)"); return "[EXPEDITION] FORCE RESET a stuck expedition (" + text + "). The guard is open again and any waiting caller (SpawnKit's menu/verb chain) was handed an aborted-trip result — nothing chains onto a force reset. If the party is not home, recover with 'goto '. Please report the log — Begin is supposed to release the guard on its own."; } private static void StampRoomProperty() { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown _roomPropStamped = false; try { if (PhotonNetwork.inRoom && PhotonNetwork.room != null) { Hashtable val = new Hashtable(); val[(object)"CK_EXPED"] = _generation; PhotonNetwork.room.SetCustomProperties(val, (Hashtable)null, false); _roomPropStamped = true; DonorRuntime.Log.LogMessage((object)(string.Format("[EXPEDITION] stamped room property {0}={1} — ", "CK_EXPED", _generation) + "guests with [Expedition] GuestAutoContinue=true will auto-pass their continue gates.")); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] stamping the CK_EXPED room property threw (" + ex.GetType().Name + ": " + ex.Message + ") — guests must pass their gates by hand.")); } } private static void ClearRoomProperty() { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown if (!_roomPropStamped) { return; } _roomPropStamped = false; try { if (PhotonNetwork.inRoom && PhotonNetwork.room != null) { Hashtable val = new Hashtable(); val[(object)"CK_EXPED"] = null; PhotonNetwork.room.SetCustomProperties(val, (Hashtable)null, false); DonorRuntime.Log.LogMessage((object)"[EXPEDITION] cleared room property CK_EXPED."); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] clearing the CK_EXPED room property threw (" + ex.GetType().Name + ": " + ex.Message + ").")); } } private static bool LoaderBusy() { NetworkLevelLoader instance = NetworkLevelLoader.Instance; if ((Object)(object)instance == (Object)null) { return false; } try { return instance.InLoading || instance.m_prepingLoadLevel; } catch { return false; } } private static void HandleSceneLoadingDone() { //IL_0038: 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) if (InProgress) { ManualLogSource log = DonorRuntime.Log; string text = $"[EXPEDITION] phase: scene-loading-done #{++_sceneDoneCount} "; object arg = _leg; Scene activeScene = SceneManager.GetActiveScene(); log.LogMessage((object)(text + $"(leg {arg}, scene '{((Scene)(ref activeScene)).name}', frame {Time.frameCount}).")); } } private static void HandleGameplayLoadingDone() { //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_00a3: Unknown result type (might be due to invalid IL or missing references) if (!InProgress) { return; } Scene activeScene = SceneManager.GetActiveScene(); string b = ((_leg == 1) ? _donorScene : _homeScene); DonorRuntime.Log.LogMessage((object)($"[EXPEDITION] phase: gameplay-loading-done #{++_gameplayDoneCount} " + $"(leg {_leg}, scene '{((Scene)(ref activeScene)).name}', frame {Time.frameCount}).")); if (string.Equals(((Scene)(ref activeScene)).name, b, StringComparison.OrdinalIgnoreCase)) { _gameplayDoneLeg = _leg; if (_leg == 1 && !_payloadRan) { _payloadRan = true; _result = RunPayload(activeScene, _payload); } } } private static void HandleResumeAfterLoading() { //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_0010: 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_0079: 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_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_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_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0089: 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_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Expected I4, but got Unknown //IL_0132: 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_015b: Unknown result type (might be due to invalid IL or missing references) if (!InProgress) { return; } Scene activeScene = SceneManager.GetActiveScene(); State val = new State { Leg = _leg, ActiveScene = ((Scene)(ref activeScene)).name, HomeScene = _homeScene, DonorScene = _donorScene, ReturnRequested = _returnRequested, Restoring = _restoring, PayloadRan = _payloadRan, LoaderBusy = LoaderBusy() }; Decision val2 = ExpeditionResume.Decide(val); LogResume(val, val2); Action action = val2.Action; switch (action - 1) { case 0: if (!_payloadRan) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] payload window was missed — running it at resume instead."); _payloadRan = true; _result = RunPayload(activeScene, _payload); } _leg = 2; DonorRuntime.Log.LogMessage((object)("[EXPEDITION] donor leg done — returning to '" + _homeScene + "'.")); DonorRuntime.Runner.StartCoroutine(ReturnHomeNextFrame(_generation)); break; case 1: _restoring = true; DonorRuntime.Runner.StartCoroutine(RestoreAndFinish(_generation)); break; case 2: DonorRuntime.Log.LogWarning((object)("[EXPEDITION] " + val2.Reason + " — aborting where we stand" + ((_leg == 2) ? " (delivering the payload result without position restore)." : " (no teleports)."))); Finish(val2.Reason); break; } } private static void LogResume(State s, Decision d) { //IL_0059: 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_0099: 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_00af: 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_00ec: 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) _resumeCount++; int frameCount = Time.frameCount; bool flag = _resumeCount > 1 && frameCount == _lastResumeFrame; _lastResumeFrame = frameCount; NetworkLevelLoader loader = NetworkLevelLoader.Instance; DonorRuntime.Log.LogMessage((object)($"[EXPEDITION][RESUME] #{_resumeCount} leg={s.Leg} scene='{s.ActiveScene}'" + string.Format(" frame={0}{1}", frameCount, flag ? " (SAME FRAME as the previous resume — a duplicate delegate invoke)" : "") + $" loaderBusy={s.LoaderBusy} returnRequested={s.ReturnRequested} restoring={s.Restoring}" + " lastLoaded='" + Safe(delegate { NetworkLevelLoader obj = loader; return (obj == null) ? null : obj.TargetScene; }) + "'" + $" → {d.Action}: {d.Reason}")); try { DonorRuntime.Log.LogMessage((object)($"[EXPEDITION][RESUME] #{_resumeCount} invoked from:\n" + new StackTrace(fNeedFileInfo: false))); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION][RESUME] stack trace unavailable (" + ex.GetType().Name + ").")); } static string Safe(Func f) { try { return f()?.ToString() ?? "null"; } catch (Exception ex2) { return "threw:" + ex2.GetType().Name; } } } private static IEnumerator ReturnHomeNextFrame(int gen) { yield return null; DonorRuntime.Log.LogMessage((object)$"[EXPEDITION] return leg: dwelling {ReturnLegDelaySeconds:F1}s before requesting '{_homeScene}'."); if (ReturnLegDelaySeconds > 0f) { yield return (object)new WaitForSeconds(ReturnLegDelaySeconds); } if (!InProgress || gen != _generation) { bool flag = string.Equals(_endScene, _donorScene, StringComparison.OrdinalIgnoreCase); string text = "(ended in '" + _endScene + "' — " + _endReason + "); the trip home was never requested."; if (flag) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] return leg: the dwell expired but this expedition was already torn down IN THE DONOR SCENE " + text + " This is the Bug-25 shape — the resume ladder let a teardown through. Report the log.")); } else { DonorRuntime.Log.LogMessage((object)("[EXPEDITION] return leg: the dwell expired but the trip had already ended " + text + " Nothing to do — this is the expected tail of a deliberate abort, not Bug 25.")); } } else { NetworkLevelLoader instance = NetworkLevelLoader.Instance; if ((Object)(object)instance == (Object)null) { DonorRuntime.Log.LogError((object)"[EXPEDITION] NetworkLevelLoader vanished before the return leg — aborting in the donor scene."); Finish("the level loader vanished before the return leg"); } else { RequestReturn(instance, "return leg"); DonorRuntime.Runner.StartCoroutine(ReturnWatch(gen)); } } } private static void RequestReturn(NetworkLevelLoader loader, string why) { _returnRequested = true; DonorRuntime.Log.LogMessage((object)("[EXPEDITION] " + why + ": requesting the switch back to '" + _homeScene + "'.")); loader.RequestSwitchArea(_homeScene, 0, 1.5f, false); } private static IEnumerator ReturnWatch(int gen) { while (ReturnRetrySeconds > 0f && _returnRetries < 2) { float t0 = Time.unscaledTime; while (Time.unscaledTime - t0 < ReturnRetrySeconds) { if (!InProgress || gen != _generation) { yield break; } yield return (object)new WaitForSeconds(0.5f); } if (!InProgress || gen != _generation || _leg != 2 || !IsActiveScene(_donorScene)) { break; } if (!LoaderBusy()) { NetworkLevelLoader instance = NetworkLevelLoader.Instance; if ((Object)(object)instance == (Object)null) { break; } _returnRetries++; DonorRuntime.Log.LogWarning((object)($"[EXPEDITION] the return to '{_homeScene}' was requested {ReturnRetrySeconds:F0}s ago " + "but no load ever started and we are still in '" + _donorScene + "' — re-requesting " + $"(retry {_returnRetries}/{2}). Leaving the player in a donor region is the Bug-25 failure.")); RequestReturn(instance, $"return retry {_returnRetries}"); } } } private static bool IsActiveScene(string scene) { //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) if (!string.IsNullOrEmpty(scene)) { Scene activeScene = SceneManager.GetActiveScene(); return string.Equals(((Scene)(ref activeScene)).name, scene, StringComparison.OrdinalIgnoreCase); } return false; } private static IEnumerator RestoreAndFinish(int gen) { float t0 = Time.unscaledTime; bool sane = false; while (Time.unscaledTime - t0 < 30f) { if (!InProgress || gen != _generation) { yield break; } Character val = ResolveMark(_marks[0]); if ((Object)(object)val != (Object)null && DonorRuntime.IsSanePosition(((Component)val).transform.position)) { sane = true; break; } yield return (object)new WaitForSeconds(0.5f); } if (InProgress && gen == _generation) { int num = RestoreMarks(); if (sane) { DonorRuntime.Log.LogMessage((object)$"[EXPEDITION] home in '{_homeScene}' — restored {num}/{_marks.Count} player position(s)."); } else { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] home in '" + _homeScene + "' but players never reached a sane position within 30s " + $"(void-staging?) — restoring {num}/{_marks.Count} player position(s) anyway; any void-restore is attributable to THIS timeout.")); } Finish(string.Format("landed home, restored {0}/{1}{2}", num, _marks.Count, sane ? "" : " [sane-pos timeout]")); } } private static int RestoreMarks() { //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) int num = 0; foreach (PlayerMark mark in _marks) { Character val = ResolveMark(mark); if ((Object)(object)val == (Object)null) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] player '" + mark.Uid + "' not found after the return — skipping their restore.")); continue; } val.Teleport(mark.Pos, mark.Rot); num++; } return num; } private static IEnumerator AutoContinue(int gen) { int passedLeg = 0; float lastDump = -999f; while (InProgress && gen == _generation) { try { NetworkLevelLoader instance = NetworkLevelLoader.Instance; bool flag = _gameplayDoneLeg == _leg; if ((Object)(object)instance != (Object)null && flag && !instance.ContinueAfterLoading && !instance.IsGameplayLoading && instance.AllPlayerDoneLoading && (Object)(object)MenuManager.Instance != (Object)null && MenuManager.Instance.IsMasterLoadingDisplayed) { instance.SetContinueAfterLoading(); if (passedLeg != _leg) { passedLeg = _leg; DonorRuntime.Log.LogMessage((object)$"[EXPEDITION] auto-passed the continue gate (leg {_leg}) — no keypress needed."); } } else if ((Object)(object)instance != (Object)null && Time.unscaledTime - lastDump >= 10f) { lastDump = Time.unscaledTime; DumpGateState(instance, flag); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] gate poll threw (poll continues): " + ex.GetType().Name + ": " + ex.Message)); } yield return (object)new WaitForSeconds(0.25f); } } private static void DumpGateState(NetworkLevelLoader loader, bool legGateReached) { DonorRuntime.Log.LogMessage((object)("[EXPEDITION][GATE] leg=" + _leg + " legGateReached=" + legGateReached + " continue=" + Safe(() => loader.ContinueAfterLoading) + " gameplayLoading=" + Safe(() => loader.IsGameplayLoading) + " sceneLoading=" + Safe(() => loader.IsSceneLoading) + " allPlayersDone=" + Safe(() => loader.AllPlayerDoneLoading) + " allReadyToContinue=" + Safe(() => loader.AllPlayerReadyToContinue) + " doneIds=[" + Safe(() => string.Join(",", loader.m_doneLoadingPlayers)) + "] readyIds=[" + Safe(() => string.Join(",", loader.m_readyToContinuePlayers)) + "] waitingOthers=" + Safe(() => loader.m_waitingForOtherPlayers) + " masterLoadingUI=" + Safe(() => (Object)(object)MenuManager.Instance != (Object)null && MenuManager.Instance.IsMasterLoadingDisplayed) + " overallDone=" + Safe(() => loader.IsOverallLoadingDone) + " charsInit=" + Safe(() => (Object)(object)CharacterManager.Instance != (Object)null && CharacterManager.Instance.IsAllCharactersInitialized) + " itemsSynced=" + Safe(() => (Object)(object)ItemManager.Instance != (Object)null && ItemManager.Instance.IsAllItemSynced) + " scene='" + Safe(delegate { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); return ((Scene)(ref activeScene)).name; }) + "'")); static string Safe(Func f) { try { return f()?.ToString() ?? "null"; } catch (Exception ex) { return "threw:" + ex.GetType().Name; } } } private static IEnumerator Watchdog(int gen) { float t0 = Time.unscaledTime; string name; while (true) { if (InProgress && gen == _generation && Time.unscaledTime - t0 < 300f) { yield return (object)new WaitForSeconds(1f); continue; } if (!InProgress || gen != _generation) { yield break; } Scene activeScene = SceneManager.GetActiveScene(); name = ((Scene)(ref activeScene)).name; if (_leg != 2 || _rescued || LoaderBusy() || !string.Equals(name, _donorScene, StringComparison.OrdinalIgnoreCase)) { break; } NetworkLevelLoader instance = NetworkLevelLoader.Instance; if (!((Object)(object)instance != (Object)null)) { break; } _rescued = true; DonorRuntime.Log.LogError((object)($"[EXPEDITION] watchdog: wedged on the return leg after {300f:F0}s and still in " + "'" + name + "' — one last rescue attempt to get home to '" + _homeScene + "' before tearing down " + $"({60f:F0}s grace). Tearing down here would leave you in a donor region (Bug 25).")); RequestReturn(instance, "watchdog rescue"); t0 = Time.unscaledTime - 300f + 60f; } int num = (string.Equals(name, _homeScene, StringComparison.OrdinalIgnoreCase) ? RestoreMarks() : (-1)); DonorRuntime.Log.LogError((object)($"[EXPEDITION] watchdog: still on leg {_leg} after {300f:F0}s — tearing down where we stand " + "(active scene '" + name + "'" + ((num >= 0) ? $"; restored {num}/{_marks.Count} player position(s)" : "; no restore — not home") + "). The payload result" + ((_result != null) ? " IS" : " is NOT") + " delivered.")); Finish($"watchdog timeout on leg {_leg}"); } private static object RunPayload(Scene donor, Func payload) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) try { object obj = payload(donor); DonorRuntime.Log.LogMessage((object)string.Format("[EXPEDITION] payload done in '{0}' (result={1}).", ((Scene)(ref donor)).name, obj ?? "null")); return obj; } catch (Exception arg) { DonorRuntime.Log.LogError((object)$"[EXPEDITION] payload threw in '{((Scene)(ref donor)).name}': {arg}"); return null; } } private static Character ResolveMark(PlayerMark mark) { if ((Object)(object)mark.Ch != (Object)null) { return mark.Ch; } CharacterManager instance = CharacterManager.Instance; if (!((Object)(object)instance != (Object)null)) { return null; } return instance.GetCharacter(mark.Uid); } private static void Finish(string reason) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: 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: Expected O, but got Unknown //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Expected O, but got Unknown //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Expected O, but got Unknown //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_0123: Expected O, but got Unknown //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Expected O, but got Unknown //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Expected O, but got Unknown Scene activeScene = SceneManager.GetActiveScene(); string name = ((Scene)(ref activeScene)).name; bool flag = (LastTripEndedHome = string.Equals(name, _homeScene, StringComparison.OrdinalIgnoreCase)); _endScene = name; _endReason = reason; Character val = ((_marks.Count > 0) ? ResolveMark(_marks[0]) : null); if (flag) { DonorRuntime.Log.LogMessage((object)$"[EXPEDITION] teardown in '{name}' (leg {_leg}) — {reason}."); } else { DonorRuntime.Log.LogError((object)($"[EXPEDITION] teardown in '{name}' (leg {_leg}) — {reason}. " + "THE PARTY IS NOT HOME ('" + _homeScene + "'): the game saves on area changes, so this scene may be baked into the save. Recover with 'goto " + _homeScene + "'. (Bug-25 class — please report the log.)")); } if ((Object)(object)_loader != (Object)null) { NetworkLevelLoader loader = _loader; loader.onSceneLoadingDone = (UnityAction)Delegate.Remove((Delegate?)(object)loader.onSceneLoadingDone, (Delegate?)new UnityAction(HandleSceneLoadingDone)); NetworkLevelLoader loader2 = _loader; loader2.onGameplayLoadingDone = (UnityAction)Delegate.Remove((Delegate?)(object)loader2.onGameplayLoadingDone, (Delegate?)new UnityAction(HandleGameplayLoadingDone)); NetworkLevelLoader loader3 = _loader; loader3.OnResumeAfterLoading = (UnityAction)Delegate.Remove((Delegate?)(object)loader3.OnResumeAfterLoading, (Delegate?)new UnityAction(HandleResumeAfterLoading)); } ClearRoomProperty(); Action onDone = _onDone; object result = _result; InProgress = false; _generation++; _loader = null; _payload = null; _onDone = null; _result = null; _marks.Clear(); _leg = 0; _gameplayDoneLeg = 0; _returnRequested = false; _restoring = false; _returnRetries = 0; _rescued = false; _resumeCount = 0; _lastResumeFrame = 0; _sceneDoneCount = 0; _gameplayDoneCount = 0; if (!flag) { try { Notify.Player(val, "The expedition could not bring you home. You are in " + name + "."); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] toast failed: " + ex.Message)); } } try { onDone?.Invoke(result); } catch (Exception ex2) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] onDone threw: " + ex2.Message)); } } } public static class ExpeditionGuestGate { private static float _next; private static NetworkLevelLoader _subscribed; private static bool _gateArmed; private static bool _sawExpedition; private static float _activeSince; private static bool _staleWarned; private static int _generation; private const float StalePropSeconds = 420f; public static void Tick() { //IL_0170: 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_01ed: Unknown result type (might be due to invalid IL or missing references) //IL_020f: Unknown result type (might be due to invalid IL or missing references) //IL_0214: Unknown result type (might be due to invalid IL or missing references) if (Time.unscaledTime < _next) { return; } _next = Time.unscaledTime + 0.25f; try { if (!PhotonNetwork.isNonMasterClientInRoom) { _gateArmed = false; return; } NetworkLevelLoader instance = NetworkLevelLoader.Instance; if ((Object)(object)instance == (Object)null) { return; } EnsureSubscribed(instance); if (!HostExpeditionActive(out var generation)) { _gateArmed = false; _sawExpedition = false; _activeSince = 0f; _staleWarned = false; _generation = 0; return; } if (generation != _generation) { _generation = generation; _activeSince = 0f; _staleWarned = false; _sawExpedition = false; } if (_activeSince <= 0f) { _activeSince = Time.unscaledTime; } if (Time.unscaledTime - _activeSince > 420f) { if (!_staleWarned) { _staleWarned = true; DonorRuntime.Log.LogWarning((object)("[EXPEDITION] guest: room property CK_EXPED has been " + $"set for over {420f:0}s — longer than any legitimate trip (host watchdog + grace). " + "Treating it as STALE (the host's clear likely failed); continue gates are manual again.")); } _gateArmed = false; return; } if (!_sawExpedition) { _sawExpedition = true; DonorRuntime.Log.LogMessage((object)(string.Format("[EXPEDITION] guest: the host stamped {0}={1} — ", "CK_EXPED", generation) + "an expedition is in flight; this client will auto-pass its continue gates " + $"(GuestAutoContinue={DkConfig.Expedition.GuestAutoContinue.Value}).")); } if (instance.IsGameplayLoading) { _gateArmed = false; return; } GuestGateState val = new GuestGateState { Enabled = DkConfig.Expedition.GuestAutoContinue.Value, IsGuest = true, HostExpeditionActive = true, GateArmed = _gateArmed, ContinueAlreadySet = instance.ContinueAfterLoading, GameplayLoading = instance.IsGameplayLoading, AllPlayersDone = instance.AllPlayerDoneLoading, MasterLoadingDisplayed = ((Object)(object)MenuManager.Instance != (Object)null && MenuManager.Instance.IsMasterLoadingDisplayed) }; if (ExpeditionCoop.ShouldGuestContinue(ref val)) { _gateArmed = false; instance.SetContinueAfterLoading(); ManualLogSource log = DonorRuntime.Log; Scene activeScene = SceneManager.GetActiveScene(); log.LogMessage((object)("[EXPEDITION] guest auto-continue: passed this machine's continue gate for the host's expedition (scene '" + ((Scene)(ref activeScene)).name + "') — no keypress needed.")); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] guest gate poll threw (poll continues): " + ex.GetType().Name + ": " + ex.Message)); } } private static void EnsureSubscribed(NetworkLevelLoader loader) { //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Expected O, but got Unknown //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Expected O, but got Unknown //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Expected O, but got Unknown //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Expected O, but got Unknown if (_subscribed == loader) { return; } if ((Object)(object)_subscribed != (Object)null) { try { NetworkLevelLoader subscribed = _subscribed; subscribed.onGameplayLoadingDone = (UnityAction)Delegate.Remove((Delegate?)(object)subscribed.onGameplayLoadingDone, (Delegate?)new UnityAction(OnGameplayDone)); } catch { } } loader.onGameplayLoadingDone = (UnityAction)Delegate.Combine((Delegate?)(object)loader.onGameplayLoadingDone, (Delegate?)new UnityAction(OnGameplayDone)); _subscribed = loader; } private static void OnGameplayDone() { _gateArmed = true; } private static bool HostExpeditionActive(out int generation) { generation = 0; RoomInfo room = (RoomInfo)(object)PhotonNetwork.room; if (room == null || room.CustomProperties == null || !((Dictionary)(object)room.CustomProperties).ContainsKey((object)"CK_EXPED")) { return false; } try { object obj = room.CustomProperties[(object)"CK_EXPED"]; if (obj != null) { generation = Convert.ToInt32(obj); } } catch { generation = 0; } return true; } } public static class ExpeditionManifest { private static List _scenes; public static IReadOnlyList Scenes { get { EnsureLoaded(); return _scenes; } } private static string PathFor() { return Path.Combine(Paths.ConfigPath, "ck_expeditions.txt"); } private static string LegacyPathFor() { return Path.Combine(Paths.ConfigPath, "bw_expeditions.txt"); } private static void EnsureLoaded() { if (_scenes != null) { return; } try { string path = PathFor(); string path2 = LegacyPathFor(); if (File.Exists(path)) { _scenes = ExpeditionLog.Parse(File.ReadAllText(path)); if (File.Exists(path2)) { DonorRuntime.Log.LogMessage((object)"[EXPEDITION] manifest: ck_expeditions.txt is authoritative — the stray legacy bw_expeditions.txt is ignored (safe to delete)."); } } else if (File.Exists(path2)) { _scenes = ExpeditionLog.Parse(File.ReadAllText(path2)); File.WriteAllText(path, ExpeditionLog.Format((IEnumerable)_scenes)); DonorRuntime.Log.LogWarning((object)($"[EXPEDITION] manifest migrated: {_scenes.Count} scene(s) copied from legacy bw_expeditions.txt " + "into ck_expeditions.txt (DonorKit owns the expedition manifest now — docs/expedition-ownership-plan.md, then lane 5E-2). The legacy file is left in place but no longer read.")); } else { _scenes = new List(); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] manifest read failed: " + ex.Message)); _scenes = new List(); } } public static void Record(string scene) { EnsureLoaded(); if (!ExpeditionLog.Append(_scenes, scene)) { return; } try { File.WriteAllText(PathFor(), ExpeditionLog.Format((IEnumerable)_scenes)); DonorRuntime.Log.LogMessage((object)$"[EXPEDITION] manifest: recorded '{scene}' ({_scenes.Count} scene(s) in ck_expeditions.txt)."); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] manifest write failed: " + ex.Message)); } } public static string Dump() { EnsureLoaded(); if (_scenes.Count != 0) { return string.Format("[EXPEDITION] manifest ({0} dumped scene(s)): {1}", _scenes.Count, string.Join(", ", _scenes)); } return "[EXPEDITION] manifest: no dumped scenes recorded yet (ck_expeditions.txt)."; } } public static class ExpeditionOrchestrator { public struct TripResult { public string Scene; public bool PayloadRan; public int Built; public bool EndedHome; public bool Ok { get { if (PayloadRan) { return EndedHome; } return false; } } } public static Func ActiveSpeciesProvider; private static readonly List s_registeredAlwaysWarm = new List(); public static bool AutoWarmDeferred; private static bool _autoWarmEvaluated; private static bool _reArmOnRoomLeft; private static bool _roomWatchWired; private static string _autoWarmSummary = "not yet evaluated"; private static readonly HashSet _uncapturable = new HashSet(StringComparer.OrdinalIgnoreCase); private static readonly HashSet _sweptScenes = new HashSet(StringComparer.OrdinalIgnoreCase); private static string ActiveSpecies { get { try { return ActiveSpeciesProvider?.Invoke(); } catch { return null; } } } private static Character LocalPlayer { get { CharacterManager instance = CharacterManager.Instance; if (instance == null) { return null; } return instance.GetFirstLocalCharacter(); } } private static ManualLogSource Log => DonorRuntime.Log; public static void RegisterAlwaysWarmSpecies(string species) { if (string.IsNullOrEmpty(species)) { return; } foreach (string item in ExpeditionWarm.ParseSpeciesList(species)) { if (!s_registeredAlwaysWarm.Contains(item)) { s_registeredAlwaysWarm.Add(item); } } } internal static List EffectiveAlwaysWarm() { string text = (DkConfig.Expedition.AlwaysWarmSpecies.Value ?? "").Trim(); if (string.Equals(text, "none", StringComparison.OrdinalIgnoreCase)) { return new List(); } List list = ExpeditionWarm.ParseSpeciesList(text); if (list.Count <= 0) { return new List(s_registeredAlwaysWarm); } return list; } public static bool BeginTrip(string scene, Action onDone) { return ExpeditionHarvest.Begin(scene, (Scene donor) => CapturePayload(donor), delegate(object result) { TripResult obj = new TripResult { Scene = scene, PayloadRan = (result is int), Built = ((result is int num) ? num : 0), EndedHome = ExpeditionHarvest.LastTripEndedHome }; try { if (obj.PayloadRan) { ExpeditionManifest.Record(scene); } } catch (Exception ex) { Log.LogWarning((object)("[EXPEDITION] recording '" + scene + "' in the manifest threw: " + ex.Message)); } try { onDone?.Invoke(obj); } catch (Exception ex2) { Log.LogWarning((object)("[EXPEDITION] BeginTrip onDone threw: " + ex2.Message)); } }); } public static void RunVerb(string[] parts) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Expected I4, but got Unknown //IL_0108: 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_0143: 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_02a1: Unknown result type (might be due to invalid IL or missing references) //IL_02b8: Unknown result type (might be due to invalid IL or missing references) //IL_0352: Unknown result type (might be due to invalid IL or missing references) Command val = ExpeditionVerb.Parse(parts); Kind kind = val.Kind; switch ((int)kind) { case 0: Log.LogMessage((object)("[EXPEDITION] " + ExpeditionHarvest.Status())); Log.LogMessage((object)BodyTemplateStore.Dump()); Log.LogMessage((object)ExpeditionManifest.Dump()); Log.LogMessage((object)($"[EXPEDITION] config: CaptureOnSceneEntry={DkConfig.Expedition.CaptureOnSceneEntry.Value}, " + "AutoWarmAtBoot=" + DkConfig.Expedition.AutoWarmAtBoot.Value + ", AlwaysWarmSpecies='" + DkConfig.Expedition.AlwaysWarmSpecies.Value + "' (effective: '" + string.Join(",", EffectiveAlwaysWarm().ToArray()) + "' — cfg override ∥ consumer-registered); boot auto-warm this launch: " + _autoWarmSummary + ".")); Log.LogMessage((object)"[EXPEDITION] usage: expedition — two loading screens there and back; host/offline only; caches body templates for every species the scene donates."); return; case 1: ExpeditionHarvest.ReturnLegDelaySeconds = val.DelaySeconds; Log.LogMessage((object)$"[EXPEDITION] return-leg delay set to {val.DelaySeconds:F1}s (session only; 0 = next-frame chain)."); return; case 2: Log.LogWarning((object)"[EXPEDITION] usage: expedition delay "); return; } string target = val.Target; List similar; string scene = DonorHarvest.ResolveBuildScene(target, out similar); if (scene == null) { if (!DonorHarvest.TryGetExpeditionScenes(target, out var sceneNames, out var searchTerm)) { if (DonorHarvest.TryGetDonorScenes(target, out var sceneNames2, out var searchTerm2)) { Log.LogWarning((object)("[EXPEDITION] '" + searchTerm2 + "' has ADDITIVE donors (" + string.Join(", ", sceneNames2) + ") — the normal harvest chain covers it with no loading screens. Pass a scene name to force a trip anyway.")); } else { Log.LogWarning((object)("[EXPEDITION] '" + target + "' is neither a build-settings scene (" + ((similar.Count > 0) ? ("did you mean: " + string.Join(", ", similar)) : "see scenedump") + ") nor a species with donor-table entries.")); } return; } scene = sceneNames[0]; Log.LogMessage((object)("[EXPEDITION] species '" + searchTerm + "' → expedition donor '" + scene + "'" + ((sceneNames.Count > 1) ? $" (+{sceneNames.Count - 1} more candidate(s) in the manifest)" : "") + ".")); } Scene activeScene = SceneManager.GetActiveScene(); bool flag = string.Equals(((Scene)(ref activeScene)).name, scene, StringComparison.OrdinalIgnoreCase); if (!val.Force && !flag) { if (StorySense.StoryHeld(out var reason)) { Log.LogWarning((object)("[EXPEDITION] refusing: " + reason + ". Override with 'expedition force " + target + "' (the prologue refusal cannot be forced).")); return; } if (!StorySense.SceneVisited(scene)) { Log.LogWarning((object)("[EXPEDITION] refusing: '" + scene + "' is UNVISITED by this character — a trip would bake its first-entry story state behind the loading screen. Walk there once, or 'expedition force " + target + "'.")); return; } } else if (val.Force) { Log.LogMessage((object)"[EXPEDITION] force: bypassing the story-safety first-visit checks (the prologue backstop still applies)."); } if (!BeginTrip(scene, delegate(TripResult trip) { Log.LogMessage((object)($"[EXPEDITION] complete: {trip.Built} NEW body template(s) cached from '{scene}'." + ((trip.Built > 0) ? " A bodiless/ghost companion re-forms from the cache within ~2s." : " (0 new — a warm cache also lands here; 'expedition' shows the census.)"))); })) { Log.LogWarning((object)"[EXPEDITION] not started — see the refusal line above."); } } public static object CapturePayload(Scene donor) { //IL_00f0: Unknown result type (might be due to invalid IL or missing references) List list = DonorTable.KeysForScene(DonorHarvest.DonorScenes, ((Scene)(ref donor)).name, true); string activeSpecies = ActiveSpecies; if (!string.IsNullOrEmpty(activeSpecies)) { bool flag = false; foreach (string item in list) { if (Species.NameMatches(activeSpecies, item) || Species.NameMatches(item, activeSpecies)) { flag = true; break; } } if (!flag) { list.Add(activeSpecies); } } if (list.Count == 0) { Log.LogWarning((object)("[EXPEDITION] no donor-table species lists '" + ((Scene)(ref donor)).name + "' and no active companion to serve — nothing to capture; NOT recording this scene as swept (check DonorScenes.txt if this scene should donate).")); return null; } int num = 0; int num2 = 0; List list2 = new List(); _sweptScenes.Add(((Scene)(ref donor)).name); foreach (string item2 in list) { BodyTemplate resident = BodyTemplateStore.GetResident(item2); if (resident != null && !resident.Substituted) { num2++; continue; } int rank; Character val = DonorHarvest.FindLiveInScene(donor, item2, out rank); if (resident != null) { if ((Object)(object)val == (Object)null || rank < 2) { num2++; _uncapturable.Add(MissKey(((Scene)(ref donor)).name, item2)); continue; } Log.LogMessage((object)("[EXPEDITION] '" + ((Scene)(ref donor)).name + "' holds an exact-name '" + item2 + "' — upgrading the resident substitute template (was a '" + resident.SpeciesId + "').")); } if ((Object)(object)val == (Object)null) { list2.Add(item2); _uncapturable.Add(MissKey(((Scene)(ref donor)).name, item2)); } else if (BodyTemplateStore.Capture(val, item2) != null) { num++; } else { list2.Add(item2); _uncapturable.Add(MissKey(((Scene)(ref donor)).name, item2)); } } Log.LogMessage((object)($"[EXPEDITION] '{((Scene)(ref donor)).name}': {num} captured, {num2} already covered by the cache, " + $"{list2.Count} missing of {list.Count} key(s)" + ((list2.Count > 0) ? (" — missing: " + string.Join(", ", list2)) : "") + ".")); return num; } private static void EnsureRoomWatch() { if (_roomWatchWired) { return; } _roomWatchWired = true; Net.OnRoomChanged += delegate(RoomChange change) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) if (_reArmOnRoomLeft && change.NewRoomName == null) { _reArmOnRoomLeft = false; _autoWarmEvaluated = false; _autoWarmSummary = "re-armed (left the co-op room — the guest-role skip was room-scoped)"; Log.LogMessage((object)"[EXPEDITION] auto-warm re-armed — this machine left the co-op room, so the non-master skip no longer applies; the next player-ready re-evaluates the pass."); } }; } public static void OpportunisticCapture(string scene) { //IL_001d: 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) if (!DkConfig.Expedition.CaptureOnSceneEntry.Value || !DonorTable.IsOversized(scene) || ExpeditionHarvest.InProgress) { return; } Scene activeScene = SceneManager.GetActiveScene(); if (string.Equals(((Scene)(ref activeScene)).name, scene, StringComparison.OrdinalIgnoreCase) && SceneHasUncachedKey(scene)) { Log.LogMessage((object)("[EXPEDITION] entered expedition-tier scene '" + scene + "' with uncached species — capturing in place (zero loading screens).")); if (!BeginTrip(scene, delegate { })) { Log.LogWarning((object)"[EXPEDITION] in-place capture did not start — see the refusal line above."); } } } public static void AutoWarm() { //IL_003f: 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_0101: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) if (_autoWarmEvaluated) { return; } string value = DkConfig.Expedition.AutoWarmAtBoot.Value; Mode val = default(Mode); if (!ExpeditionWarm.TryParseMode(value, ref val)) { Log.LogWarning((object)("[EXPEDITION] AutoWarmAtBoot='" + value + "' is not off|needed|all — treating as off (AlwaysWarmSpecies still applies).")); val = (Mode)0; } List list = EffectiveAlwaysWarm(); if ((int)val == 0 && list.Count == 0) { _autoWarmEvaluated = true; _autoWarmSummary = "off"; return; } if (StorySense.StoryHeld(out var reason)) { _autoWarmSummary = "story-safety hold (" + reason + ")"; Log.LogMessage((object)("[EXPEDITION] auto-warm: story-safety hold — " + reason + ". Deferring the pass to a later scene.")); return; } _autoWarmEvaluated = true; if (PhotonNetwork.isNonMasterClientInRoom) { _autoWarmSummary = "skipped (non-master client — expeditions are host-only)"; Log.LogMessage((object)"[EXPEDITION] auto-warm skipped — only the HOST runs expeditions (client bodies come from the host's trips)."); _reArmOnRoomLeft = true; EnsureRoomWatch(); } else if ((Object)(object)DonorRuntime.Runner == (Object)null) { if (ExpeditionHarvest.InProgress) { _autoWarmSummary = "skipped (an expedition was already in progress; no coroutine host to retry)"; Log.LogMessage((object)"[EXPEDITION] auto-warm skipped — an expedition is already in progress and there is no plugin host to defer on."); } else { EvaluateAndLaunch(val, list); } } else { DonorRuntime.Runner.StartCoroutine(WarmPass(val, list)); } } private static IEnumerator WarmPass(Mode mode, List listed) { //IL_0007: 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) WarmRetryPolicy policy = new WarmRetryPolicy { MaxWaitSeconds = Mathf.Max(0f, DkConfig.Expedition.AutoWarmRetrySeconds.Value), PollSeconds = 1f }; WarmRetryState state = default(WarmRetryState); bool everDeferred = false; string why; while (true) { bool flag = WarmBlockedTransiently(out why); WarmGate val = ExpeditionWarm.WarmRetryStep(ref state, flag, ref policy); if ((int)val == 0) { if (everDeferred) { Log.LogMessage((object)$"[EXPEDITION] auto-warm: the load settled after {state.ElapsedSeconds:F0}s — warming now."); } EvaluateAndLaunch(mode, listed); yield break; } if ((int)val == 2) { break; } if (!everDeferred) { everDeferred = true; _autoWarmSummary = "deferring (waiting for an in-flight load to settle: " + why + ")"; Log.LogMessage((object)$"[EXPEDITION] auto-warm: {why} — deferring, will retry each 1s up to {policy.MaxWaitSeconds:F0}s."); } else if (state.Polls % 10 == 0) { Log.LogMessage((object)$"[EXPEDITION] auto-warm: still waiting for the load to settle ({state.ElapsedSeconds:F0}s/{policy.MaxWaitSeconds:F0}s, {why})."); } yield return (object)new WaitForSecondsRealtime(policy.PollSeconds); state = ExpeditionWarm.WarmRetryAdvance(ref state, policy.PollSeconds); } _autoWarmSummary = $"gave up after waiting {state.ElapsedSeconds:F0}s for an in-flight load to settle ({why})"; Log.LogWarning((object)($"[EXPEDITION] auto-warm: {why} stayed for the whole {policy.MaxWaitSeconds:F0}s retry budget — " + "abandoning this launch's warm pass (raise [Expedition] AutoWarmRetrySeconds if boot loads run longer).")); } private static bool WarmBlockedTransiently(out string why) { if (ExpeditionHarvest.InProgress) { why = "an expedition is already in progress"; return true; } NetworkLevelLoader instance = NetworkLevelLoader.Instance; if ((Object)(object)instance == (Object)null) { why = "the level loader isn't ready yet"; return true; } if (instance.InLoading) { why = "a vanilla load is in progress"; return true; } if (DonorPhotonGuard.WindowActive) { why = "a donor-scene harvest window is open"; return true; } why = null; return false; } private static void EvaluateAndLaunch(Mode mode, List listed) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Expected O, but got Unknown //IL_0048: 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_009f: 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_0107: Unknown result type (might be due to invalid IL or missing references) //IL_010c: 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_0110: 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_0117: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Expected I4, but got Unknown //IL_0150: 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_018b: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: 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_01f2: 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_0218: Unknown result type (might be due to invalid IL or missing references) //IL_0377: Unknown result type (might be due to invalid IL or missing references) //IL_03a1: Unknown result type (might be due to invalid IL or missing references) //IL_02d3: Unknown result type (might be due to invalid IL or missing references) //IL_028f: Unknown result type (might be due to invalid IL or missing references) if (StorySense.StoryHeld(out var reason)) { _autoWarmEvaluated = false; _autoWarmSummary = "story-safety hold (" + reason + ")"; Log.LogMessage((object)("[EXPEDITION] auto-warm: story-safety hold at launch time — " + reason + ". Deferring the pass to a later scene.")); return; } PlanInput val = new PlanInput(); val.Mode = mode; val.ManifestScenes = ExpeditionManifest.Scenes; val.SceneHasUncachedKey = SceneHasUncachedKey; val.AlwaysWarm = new List(); val.SceneVisited = StorySense.SceneVisited; PlanInput val2 = val; string activeSpecies = ActiveSpecies; if (!string.IsNullOrEmpty(activeSpecies)) { val2.ActivePet = ResolveForWarm(activeSpecies); } foreach (string item in listed) { val2.AlwaysWarm.Add(ResolveForWarm(item)); } Plan val3 = ExpeditionWarm.Decide(val2); foreach (ListDecision listDecision in val3.ListDecisions) { ListOutcome outcome = listDecision.Outcome; switch ((int)outcome) { case 0: Log.LogMessage((object)("[EXPEDITION] AlwaysWarmSpecies: '" + listDecision.Species + "' → warming '" + listDecision.Scene + "'.")); break; case 1: Log.LogMessage((object)("[EXPEDITION] AlwaysWarmSpecies: '" + listDecision.Species + "' has an ADDITIVE donor — the normal harvest chain covers it, no trip.")); break; case 2: Log.LogMessage((object)("[EXPEDITION] AlwaysWarmSpecies: '" + listDecision.Species + "' is already covered by the template cache.")); break; case 3: Log.LogMessage((object)("[EXPEDITION] AlwaysWarmSpecies: '" + listDecision.Species + "' → '" + listDecision.Scene + "' is UNVISITED by this character — story-safety deferral (the in-place capture covers it on your natural first entry; a later launch warms it once visited).")); break; case 5: Log.LogMessage((object)("[EXPEDITION] AlwaysWarmSpecies: '" + listDecision.Species + "' sits out — an active companion of another species exists (the warm fires on a petless launch, or when the companion is itself listed — D8).")); break; default: Log.LogWarning((object)("[EXPEDITION] AlwaysWarmSpecies: '" + listDecision.Species + "' resolves to NOTHING in the donor table — check the spelling (see 'expedition' / DonorScenes.txt).")); break; } } if (val3.Scenes.Count == 0) { if (val3.AnyDeferred) { _autoWarmEvaluated = false; _autoWarmSummary = string.Format("mode={0}: all trips deferred (unvisited donor scene(s): {1})", mode, string.Join(", ", val3.DeferredScenes)); Log.LogMessage((object)("[EXPEDITION] auto-warm (" + _autoWarmSummary + ").")); } else { _autoWarmSummary = string.Format("mode={0}{1}: nothing to warm", mode, (listed.Count > 0) ? $", list={listed.Count}" : ""); Log.LogMessage((object)("[EXPEDITION] auto-warm (" + _autoWarmSummary + ").")); } } else { if (val3.AnyDeferred) { Log.LogMessage((object)($"[EXPEDITION] auto-warm: {val3.DeferredScenes.Count} trip(s) deferred for story safety " + "(" + string.Join(", ", val3.DeferredScenes) + ") — covered by in-place capture on natural entry.")); } _autoWarmSummary = string.Format("mode={0}: warming {1}", mode, string.Join(", ", val3.Scenes)); Log.LogMessage((object)string.Format("[EXPEDITION] auto-warm (mode={0}): {1} trip(s) — {2}.", mode, val3.Scenes.Count, string.Join(", ", val3.Scenes))); WarmChain(val3.Scenes, 0); } } private static void WarmChain(List scenes, int index, bool retriedThisIndex = false) { if (index >= scenes.Count) { Log.LogMessage((object)"[EXPEDITION] auto-warm chain complete."); return; } string scene = scenes[index]; Notify.Player(LocalPlayer, "Your companion's kin dwell far away — fetching a body (" + scene + ")..."); if (!BeginTrip(scene, delegate(TripResult trip) { Log.LogMessage((object)($"[EXPEDITION] auto-warm: '{scene}' done ({trip.Built} new template(s)), " + $"{scenes.Count - index - 1} trip(s) remaining.")); if (!trip.EndedHome) { Log.LogError((object)("[EXPEDITION] auto-warm: '" + scene + "' did not bring the party home — STOPPING the chain " + $"({scenes.Count - index - 1} scene(s) unwarmed). Continuing would treat the current scene as " + "'home' for the next trip and strand you there permanently. See the teardown line above.")); } else { DonorRuntime.Runner.StartCoroutine(WarmChainNext(scenes, index + 1)); } })) { if (!retriedThisIndex && (Object)(object)DonorRuntime.Runner != (Object)null && WarmBlockedTransiently(out var why)) { Log.LogWarning((object)("[EXPEDITION] auto-warm: '" + scene + "' would not start (" + why + ") — re-queuing this scene once after the in-flight load settles (same bounded retry as the initial pass).")); DonorRuntime.Runner.StartCoroutine(WarmChainRetry(scenes, index)); } else { Log.LogError((object)("[EXPEDITION] auto-warm: '" + scene + "' would not start (see the refusal line above) — " + $"chain stopped, {scenes.Count - index} scene(s) unwarmed.")); } } } private static IEnumerator WarmChainNext(List scenes, int index) { yield return (object)new WaitForSeconds(Mathf.Max(2f, ExpeditionHarvest.ReturnLegDelaySeconds)); WarmChain(scenes, index); } private static IEnumerator WarmChainRetry(List scenes, int index) { WarmRetryPolicy policy = new WarmRetryPolicy { MaxWaitSeconds = Mathf.Max(0f, DkConfig.Expedition.AutoWarmRetrySeconds.Value), PollSeconds = 1f }; WarmRetryState state = default(WarmRetryState); string why; while (true) { bool flag = WarmBlockedTransiently(out why); WarmGate val = ExpeditionWarm.WarmRetryStep(ref state, flag, ref policy); if ((int)val == 0) { WarmChain(scenes, index, retriedThisIndex: true); yield break; } if ((int)val == 2) { break; } yield return (object)new WaitForSecondsRealtime(policy.PollSeconds); state = ExpeditionWarm.WarmRetryAdvance(ref state, policy.PollSeconds); } Log.LogError((object)("[EXPEDITION] auto-warm: '" + scenes[index] + "' stayed blocked (" + why + ") for the whole " + $"{policy.MaxWaitSeconds:F0}s retry budget — chain stopped, {scenes.Count - index} scene(s) unwarmed " + "(raise [Expedition] AutoWarmRetrySeconds if boot loads run longer).")); } internal static SpeciesResolve ResolveForWarm(string species) { //IL_0002: 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_0011: 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_007a: 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) SpeciesResolve val = new SpeciesResolve { Species = species }; string text = default(string); List list = default(List); if (SpeciesTable.TryResolveKey>(DonorHarvest.DonorScenes, species, ref text, ref list, (string)null) && list != null && list.Count > 0) { List list3 = default(List); List list2 = DonorTable.FilterViable((IEnumerable)list, ref list3); val.HasAdditiveDonor = list2.Count > 0; val.HasExpeditionScenes = list3.Count > 0; val.FirstExpeditionScene = ((list3.Count > 0) ? list3[0] : null); val.DonorSceneVisited = val.FirstExpeditionScene != null && StorySense.SceneVisited(val.FirstExpeditionScene); } val.HasCachedTemplate = BodyTemplateStore.TryResolve(species, out var _); return val; } private static string MissKey(string scene, string key) { return scene + "\0" + key; } public static void ForgetMisses() { _uncapturable.Clear(); _sweptScenes.Clear(); } private static bool SceneHasUncachedKey(string scene) { bool flag = _sweptScenes.Contains(scene); foreach (string item in DonorTable.KeysForScene(DonorHarvest.DonorScenes, scene, true)) { BodyTemplate resident = BodyTemplateStore.GetResident(item); if ((resident == null || resident.Substituted) && (!flag || !_uncapturable.Contains(MissKey(scene, item)))) { return true; } } return false; } } [BepInPlugin("cobalt.donorkit", "DonorKit", "0.1.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string GUID = "cobalt.donorkit"; public const string NAME = "DonorKit"; public const string VERSION = "0.1.1"; internal static ManualLogSource Log = Logger.CreateLogSource("DonorKit"); internal static Plugin Instance; private CommandRegistry _commands; private CommandChannel _channel; internal void Awake() { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Expected O, but got Unknown //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; DkConfig.BindAll(((BaseUnityPlugin)this).Config); new Harmony("cobalt.donorkit").PatchAll(typeof(Plugin).Assembly); _commands = new CommandRegistry(Log); RegisterVerbs(); _channel = new CommandChannel("DonorKit_cmd.txt", Log, _commands, 0.5f, true, true); SceneManager.sceneLoaded += OnSceneLoaded; Log.LogMessage((object)"DonorKit 0.1.1 loaded — donor-scene harvest engine; 'help' in BepInEx/config/DonorKit_cmd.txt lists the verbs."); Log.LogMessage((object)("[DONORKIT] build " + BuildStamp.Read(((object)this).GetType().Assembly) + " @ " + ((object)this).GetType().Assembly.Location)); } internal void Update() { _channel.Tick(); ExpeditionGuestGate.Tick(); } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { //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) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Invalid comparison between Unknown and I4 if ((int)mode != 1 && Scenes.IsGameplay(((Scene)(ref scene)).name)) { ((MonoBehaviour)this).StartCoroutine(Lifecycle.WhenPlayerReady((Func)delegate { CharacterManager instance = CharacterManager.Instance; return (instance == null) ? null : instance.GetFirstLocalCharacter(); }, (Action)delegate { OnGameplayPlayerReady(((Scene)(ref scene)).name); }, (Action)delegate(string reason) { Log.LogMessage((object)("[EXPEDITION] player not ready in '" + ((Scene)(ref scene)).name + "' (" + reason + ") — kit capture/auto-warm hook skipped.")); }, 30f, (object)this)); } } private void OnGameplayPlayerReady(string scene) { ExpeditionOrchestrator.OpportunisticCapture(scene); if (!ExpeditionOrchestrator.AutoWarmDeferred) { ExpeditionOrchestrator.AutoWarm(); } } private void RegisterVerbs() { DonorVerbs.RegisterAll(_commands); _commands.Register("scenedump", "List every build scene (donor-scene name resolution).", (Action)delegate { DonorSceneTable.DumpBuildScenes(); }); _commands.Register("selftest", "Run DonorKit's in-game self-test.", (Action)delegate { SelfTest(); }); } private void SelfTest() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown SelfTestHarness val = new SelfTestHarness(Log); val.Begin("DonorKit"); try { val.Check("donor table non-empty", DonorHarvest.DonorScenes.Count > 0); val.Check("no harvest window leaked open", !DonorPhotonGuard.WindowActive); } catch (Exception ex) { val.Exception(ex); } val.Done(); } } public static class RagdollRig { public static string RestoreJoints(GameObject donorRoot, GameObject cloneRoot, Action warn) { //IL_0191: Unknown result type (might be due to invalid IL or missing references) //IL_019f: 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_01d2: 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_01ee: Unknown result type (might be due to invalid IL or missing references) //IL_01fc: Unknown result type (might be due to invalid IL or missing references) //IL_020a: Unknown result type (might be due to invalid IL or missing references) //IL_0218: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)donorRoot == (Object)null || (Object)(object)cloneRoot == (Object)null) { return null; } try { CharacterJoint[] componentsInChildren = cloneRoot.GetComponentsInChildren(true); if (componentsInChildren.Length != 0) { return $"authored joints intact ({componentsInChildren.Length})"; } CharacterJointManager[] componentsInChildren2 = donorRoot.GetComponentsInChildren(true); if (componentsInChildren2.Length == 0) { return null; } List list = new List(); CharacterJointManager[] array = componentsInChildren2; foreach (CharacterJointManager val in array) { if ((Object)(object)val != (Object)null && val.m_hasJoint) { list.Add(val); } } if (list.Count == 0) { warn?.Invoke($"donor has {componentsInChildren2.Length} CharacterJointManager(s) but NO joint caches — " + "the donor is itself a broken clone; spawns of this template will still gumby on death."); return "donor caches empty — NOT restored"; } int num = 0; int num2 = 0; List path = new List(); foreach (CharacterJointManager item in list) { Transform val2 = Map(donorRoot.transform, cloneRoot.transform, ((Component)item).transform, path); if ((Object)(object)val2 == (Object)null || ((Object)val2).name != ((Object)((Component)item).transform).name) { num2++; continue; } Rigidbody val3 = null; if ((Object)(object)item.m_connectedBody != (Object)null) { Transform val4 = Map(donorRoot.transform, cloneRoot.transform, ((Component)item.m_connectedBody).transform, path); if ((Object)(object)val4 != (Object)null) { val3 = ((Component)val4).GetComponent(); } if ((Object)(object)val3 == (Object)null) { num2++; continue; } } val2.localPosition = item.m_initPos; val2.localRotation = item.m_initRot; CharacterJoint val5 = ((Component)val2).gameObject.AddComponent(); ((Joint)val5).connectedBody = val3; ((Joint)val5).anchor = item.m_anchor; ((Joint)val5).axis = item.m_axis; val5.swingAxis = item.m_swingAxis; val5.lowTwistLimit = item.m_lowTwistLimit; val5.highTwistLimit = item.m_highTwistLimit; val5.swing1Limit = item.m_swing1Limit; val5.swing2Limit = item.m_swing2Limit; num++; } int num3 = 0; CharacterJointManager[] componentsInChildren3 = cloneRoot.GetComponentsInChildren(true); foreach (CharacterJointManager val6 in componentsInChildren3) { if ((Object)(object)val6 != (Object)null) { Object.DestroyImmediate((Object)(object)val6); num3++; } } if (num2 > 0) { warn?.Invoke($"{num2} ragdoll bone(s) could not be mapped donor->clone (hierarchy drift?) — restored the other {num}."); } return $"restored {num} joint(s), stripped {num3} stale manager(s)"; } catch (Exception ex) { warn?.Invoke("joint restore threw (" + ex.GetType().Name + ": " + ex.Message + ") — template kept, may gumby on death."); return "restore FAILED (see warning)"; } } internal static Transform Map(Transform donorRoot, Transform cloneRoot, Transform donorNode, List path) { path.Clear(); Transform val = donorNode; while ((Object)(object)val != (Object)null && (Object)(object)val != (Object)(object)donorRoot) { path.Add(val.GetSiblingIndex()); val = val.parent; } if ((Object)(object)val != (Object)(object)donorRoot) { return null; } Transform val2 = cloneRoot; for (int num = path.Count - 1; num >= 0; num--) { if (path[num] < 0 || path[num] >= val2.childCount) { return null; } val2 = val2.GetChild(path[num]); } return val2; } } public static class SkeletonRig { public sealed class ExternalBone { public SkinnedMeshRenderer CloneSmr; public int Slot; public Transform DonorBone; public bool RefEqualToDonor; public bool Repaired; } public sealed class Report { public int Smrs; public int Slots; public int Internal; public int External; public int ExternalOther; public int NullBones; public int DeadBones; public int RootBonesExternal; public int JointsExternal; public int Cloth; public readonly List Externals = new List(); public readonly List ForeignJoints = new List(); public bool Healthy => RigSurgery.IsHealthy(External + ExternalOther + DeadBones, NullBones, RootBonesExternal, JointsExternal); public string Summary => RigSurgery.Summarize(Smrs, Slots, Internal, External + ExternalOther, NullBones + DeadBones, RootBonesExternal, JointsExternal, Cloth); } public static bool RepairEnabled => DkConfig.Rig.RepairSkinnedBones?.Value ?? true; public static Report Audit(GameObject donorRoot, GameObject cloneRoot, Action warn) { Report report = new Report(); if ((Object)(object)cloneRoot == (Object)null) { return report; } try { SkinnedMeshRenderer[] componentsInChildren = cloneRoot.GetComponentsInChildren(true); SkinnedMeshRenderer[] array = (((Object)(object)donorRoot != (Object)null) ? donorRoot.GetComponentsInChildren(true) : null); report.Smrs = componentsInChildren.Length; if (array != null && array.Length != componentsInChildren.Length) { warn?.Invoke($"SMR count differs donor={array.Length} clone={componentsInChildren.Length} — pairing by index up to the shorter."); } for (int i = 0; i < componentsInChildren.Length; i++) { SkinnedMeshRenderer val = componentsInChildren[i]; if ((Object)(object)val == (Object)null) { continue; } SkinnedMeshRenderer val2 = ((array != null && i < array.Length) ? array[i] : null); if ((Object)(object)val2 != (Object)null && ((Object)val2).name != ((Object)val).name) { warn?.Invoke($"SMR pairing name mismatch at index {i}: donor '{((Object)val2).name}' vs clone '{((Object)val).name}' — classifying anyway (descent checks don't depend on the pairing)."); } Transform[] bones = val.bones; Transform[] array2 = (((Object)(object)val2 != (Object)null) ? val2.bones : null); report.Slots += bones.Length; for (int j = 0; j < bones.Length; j++) { ClassifySlot(report, val, j, bones[j], (array2 != null && j < array2.Length) ? array2[j] : null, array2 != null, cloneRoot.transform); } if (val.rootBone != null) { int count = report.Externals.Count; int deadBones = report.DeadBones; ClassifySlot(report, val, -1, val.rootBone, ((Object)(object)val2 != (Object)null) ? val2.rootBone : null, array2 != null, cloneRoot.transform); if (report.Externals.Count > count || report.DeadBones > deadBones) { report.RootBonesExternal++; } } } Joint[] componentsInChildren2 = cloneRoot.GetComponentsInChildren(true); foreach (Joint val3 in componentsInChildren2) { if (!((Object)(object)val3 == (Object)null) && !((Object)(object)val3.connectedBody == (Object)null) && !IsUnder(((Component)val3.connectedBody).transform, cloneRoot.transform)) { report.JointsExternal++; report.ForeignJoints.Add(val3); } } report.Cloth = cloneRoot.GetComponentsInChildren(true).Length; } catch (Exception ex) { warn?.Invoke("rig audit threw (" + ex.GetType().Name + ": " + ex.Message + ") — report is partial."); } return report; } private static void ClassifySlot(Report rep, SkinnedMeshRenderer smr, int slot, Transform bone, Transform donorBone, bool havePairing, Transform cloneRoot) { if (bone == null) { if (havePairing && donorBone != null && slot >= 0) { rep.NullBones++; } return; } if ((Object)(object)bone == (Object)null) { rep.DeadBones++; return; } if (IsUnder(bone, cloneRoot)) { if (slot >= 0) { rep.Internal++; } return; } bool flag = havePairing && bone == donorBone; if (slot >= 0) { if (flag) { rep.External++; } else { rep.ExternalOther++; } } rep.Externals.Add(new ExternalBone { CloneSmr = smr, Slot = slot, DonorBone = (flag ? bone : (((Object)(object)donorBone != (Object)null) ? donorBone : bone)), RefEqualToDonor = flag }); } public static void LogForensics(Report rep, GameObject donorRoot, Action warn) { //IL_017e: Unknown result type (might be due to invalid IL or missing references) //IL_0183: Unknown result type (might be due to invalid IL or missing references) //IL_019a: Unknown result type (might be due to invalid IL or missing references) //IL_019f: Unknown result type (might be due to invalid IL or missing references) if (warn == null || rep == null || rep.Externals.Count == 0) { return; } HashSet hashSet = (((Object)(object)donorRoot != (Object)null) ? RigSurgery.RootChain(donorRoot.transform, (Func)((Transform t) => t.parent)) : null); foreach (ExternalBone external in rep.Externals) { Transform donorBone = external.DonorBone; string text = ((external.Slot < 0) ? "rootBone" : $"bones[{external.Slot}]"); if ((Object)(object)donorBone == (Object)null) { string[] obj = new string[5] { "external ", text, " of '", null, null }; SkinnedMeshRenderer cloneSmr = external.CloneSmr; obj[3] = ((cloneSmr != null) ? ((Object)cloneSmr).name : null); obj[4] = "': target unreachable (destroyed or unpaired)."; warn(string.Concat(obj)); continue; } Transform val = null; if (hashSet != null) { Transform val2 = donorBone; while ((Object)(object)val2 != (Object)null && (Object)(object)val == (Object)null) { if (hashSet.Contains(val2)) { val = val2; } val2 = val2.parent; } } string[] obj2 = new string[17] { "external ", text, " of '", null, null, null, null, null, null, null, null, null, null, null, null, null, null }; SkinnedMeshRenderer cloneSmr2 = external.CloneSmr; obj2[3] = ((cloneSmr2 != null) ? ((Object)cloneSmr2).name : null); obj2[4] = "': bone '"; obj2[5] = ((Object)donorBone).name; obj2[6] = "' path='"; obj2[7] = PathOf(donorBone); obj2[8] = "' scene='"; Scene scene = ((Component)donorBone).gameObject.scene; obj2[9] = ((Scene)(ref scene)).name; obj2[10] = "' pos="; Vector3 position = donorBone.position; obj2[11] = ((Vector3)(ref position)).ToString("F1"); obj2[12] = " refEqual="; obj2[13] = (external.RefEqualToDonor ? "Y" : "N"); obj2[14] = " lcaWithDonor='"; obj2[15] = (((Object)(object)val != (Object)null) ? ((Object)val).name : "none"); obj2[16] = "'"; warn(string.Concat(obj2)); } } public static string Repair(GameObject donorRoot, GameObject cloneRoot, Report audit, Action warn) { if ((Object)(object)cloneRoot == (Object)null || audit == null) { return "nothing to repair"; } if (audit.Externals.Count == 0 && audit.ForeignJoints.Count == 0) { return "nothing to repair"; } try { int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; int num5 = 0; HashSet hashSet = new HashSet(); Dictionary dictionary = UniqueNameIndex(cloneRoot.transform); foreach (ExternalBone external in audit.Externals) { string text = (((Object)(object)external.DonorBone != (Object)null) ? ((Object)external.DonorBone).name : null); if (text != null && dictionary.TryGetValue(text, out var value)) { WriteSlot(external, value, hashSet); external.Repaired = true; num++; } } List list = new List(); foreach (ExternalBone external2 in audit.Externals) { if (!external2.Repaired) { list.Add(external2); } } if (list.Count > 0 && (Object)(object)donorRoot != (Object)null) { HashSet hashSet2 = RigSurgery.RootChain(donorRoot.transform, (Func)((Transform t) => t.parent)); Dictionary dictionary2 = new Dictionary(); List list2 = new List(); foreach (ExternalBone item in list) { Transform val = (((Object)(object)item.DonorBone != (Object)null) ? RigSurgery.FindGraftRoot(item.DonorBone, hashSet2, (Func)((Transform t) => t.parent)) : null); if ((Object)(object)val == (Object)null) { num4++; continue; } dictionary2[item] = val; list2.Add(val); } Dictionary dictionary3 = new Dictionary(); foreach (Transform item2 in RigSurgery.DedupeGraftRoots((IEnumerable)list2, (Func)((Transform t) => t.parent))) { GameObject val2 = Object.Instantiate(((Component)item2).gameObject, cloneRoot.transform); ((Object)val2).name = ((Object)item2).name; PoseLikeDonor(val2.transform, item2, donorRoot.transform, warn); StripToTransforms(val2, warn); dictionary3[item2] = val2.transform; num2++; } List path = new List(); foreach (KeyValuePair item3 in dictionary2) { ExternalBone key = item3.Key; Transform cloneGraftRoot; Transform val3 = OutermostGraft(item3.Value, dictionary3, out cloneGraftRoot); Transform val4 = (((Object)(object)val3 != (Object)null) ? RagdollRig.Map(val3, cloneGraftRoot, key.DonorBone, path) : null); if ((Object)(object)val4 == (Object)null) { num4++; continue; } WriteSlot(key, val4, hashSet); key.Repaired = true; num3++; } } else if (list.Count > 0) { num4 += list.Count; } foreach (SkinnedMeshRenderer item4 in hashSet) { if ((Object)(object)item4 != (Object)null) { item4.updateWhenOffscreen = true; } } foreach (Joint foreignJoint in audit.ForeignJoints) { if (!((Object)(object)foreignJoint == (Object)null)) { if (warn != null) { string[] obj = new string[5] { "joint on '", ((Object)((Component)foreignJoint).gameObject).name, "' pinned to foreign body '", null, null }; Rigidbody connectedBody = foreignJoint.connectedBody; obj[3] = ((connectedBody != null) ? ((Object)connectedBody).name : null); obj[4] = "' — destroying it (partial ragdoll beats world-pinned)."; warn(string.Concat(obj)); } Object.DestroyImmediate((Object)(object)foreignJoint); num5++; } } return $"renamed {num}, grafted {num2} subtree(s), rebound {num3}, " + $"joints cut {num5}, unrepairable {num4}"; } catch (Exception ex) { warn?.Invoke("rig repair threw (" + ex.GetType().Name + ": " + ex.Message + ") — template may still be mis-skinned."); return "repair FAILED (see warning)"; } } private static void WriteSlot(ExternalBone eb, Transform target, HashSet touched) { SkinnedMeshRenderer cloneSmr = eb.CloneSmr; if ((Object)(object)cloneSmr == (Object)null) { return; } if (eb.Slot < 0) { cloneSmr.rootBone = target; } else { Transform[] bones = cloneSmr.bones; if (eb.Slot >= bones.Length) { return; } bones[eb.Slot] = target; cloneSmr.bones = bones; } touched.Add(cloneSmr); } private static void PoseLikeDonor(Transform graft, Transform donorSubtree, Transform donorRoot, Action warn) { //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_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_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: 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_0036: 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) //IL_003d: 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) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0074: 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_006c: 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_0096: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_008e: 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) //IL_00ad: 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_00bb: 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) graft.localPosition = donorRoot.InverseTransformPoint(donorSubtree.position); graft.localRotation = Quaternion.Inverse(donorRoot.rotation) * donorSubtree.rotation; Vector3 lossyScale = donorSubtree.lossyScale; Vector3 lossyScale2 = donorRoot.lossyScale; graft.localScale = new Vector3((lossyScale2.x != 0f) ? (lossyScale.x / lossyScale2.x) : lossyScale.x, (lossyScale2.y != 0f) ? (lossyScale.y / lossyScale2.y) : lossyScale.y, (lossyScale2.z != 0f) ? (lossyScale.z / lossyScale2.z) : lossyScale.z); if (!Approximately(lossyScale, lossyScale.x) || !Approximately(lossyScale2, lossyScale2.x)) { warn?.Invoke("graft '" + ((Object)graft).name + "': non-uniform scale (subtree " + ((Vector3)(ref lossyScale)).ToString("F2") + ", donor root " + ((Vector3)(ref lossyScale2)).ToString("F2") + ") — TRS approximation, shear is not representable."); } } private static bool Approximately(Vector3 v, float s) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) if (Mathf.Approximately(v.x, s) && Mathf.Approximately(v.y, s)) { return Mathf.Approximately(v.z, s); } return false; } private static void StripToTransforms(GameObject graft, Action warn) { Joint[] componentsInChildren = graft.GetComponentsInChildren(true); foreach (Joint val in componentsInChildren) { if ((Object)(object)val != (Object)null) { Object.DestroyImmediate((Object)(object)val); } } int num = 0; for (int j = 0; j < 3; j++) { num = 0; Component[] componentsInChildren2 = graft.GetComponentsInChildren(true); foreach (Component val2 in componentsInChildren2) { if (!((Object)(object)val2 == (Object)null) && !(val2 is Transform) && !(val2 is Collider) && !(val2 is Rigidbody)) { try { Object.DestroyImmediate((Object)(object)val2); } catch { } if ((Object)(object)val2 != (Object)null) { num++; } } } Collider[] componentsInChildren3 = graft.GetComponentsInChildren(true); foreach (Collider val3 in componentsInChildren3) { if ((Object)(object)val3 != (Object)null) { try { Object.DestroyImmediate((Object)(object)val3); } catch { num++; } } } Rigidbody[] componentsInChildren4 = graft.GetComponentsInChildren(true); foreach (Rigidbody val4 in componentsInChildren4) { if ((Object)(object)val4 != (Object)null) { try { Object.DestroyImmediate((Object)(object)val4); } catch { num++; } } } if (num == 0) { break; } } if (num > 0) { warn?.Invoke($"graft '{((Object)graft).name}': {num} component(s) refused to strip — they ride the template."); } } private static Dictionary UniqueNameIndex(Transform root) { Dictionary dictionary = new Dictionary(); HashSet hashSet = new HashSet(); Transform[] componentsInChildren = ((Component)root).GetComponentsInChildren(true); foreach (Transform val in componentsInChildren) { if (!((Object)(object)val == (Object)null) && !hashSet.Contains(((Object)val).name)) { if (dictionary.ContainsKey(((Object)val).name)) { dictionary.Remove(((Object)val).name); hashSet.Add(((Object)val).name); } else { dictionary[((Object)val).name] = val; } } } return dictionary; } private static Transform OutermostGraft(Transform donorGraftRoot, Dictionary graftClone, out Transform cloneGraftRoot) { Transform val = donorGraftRoot; while ((Object)(object)val != (Object)null) { if (graftClone.TryGetValue(val, out cloneGraftRoot)) { return val; } val = val.parent; } cloneGraftRoot = null; return null; } public static string Census(GameObject root) { //IL_01bd: Unknown result type (might be due to invalid IL or missing references) //IL_01c2: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Unknown result type (might be due to invalid IL or missing references) //IL_01cb: 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_00dc: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)root == (Object)null) { return " (no object)"; } StringBuilder stringBuilder = new StringBuilder(); try { SkinnedMeshRenderer[] componentsInChildren = root.GetComponentsInChildren(true); if (componentsInChildren.Length == 0) { return " no SkinnedMeshRenderers"; } SkinnedMeshRenderer[] array = componentsInChildren; foreach (SkinnedMeshRenderer val in array) { if ((Object)(object)val == (Object)null) { continue; } Transform[] bones = val.bones; int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; List list = new List(); Transform[] array2 = bones; foreach (Transform val2 in array2) { if (val2 == null) { num4++; continue; } if ((Object)(object)val2 == (Object)null) { num++; continue; } if (IsUnder(val2, root.transform)) { num3++; continue; } num2++; if (list.Count < 4) { string name = ((Object)val2).name; Scene scene = ((Component)val2).gameObject.scene; list.Add("'" + name + "'@" + ((Scene)(ref scene)).name); } } string text = ((val.rootBone == null) ? "null" : (((Object)(object)val.rootBone == (Object)null) ? "DEAD" : (IsUnder(val.rootBone, root.transform) ? "OK" : "EXTERNAL"))); if (stringBuilder.Length > 0) { stringBuilder.Append('\n'); } string text2 = $" SMR '{((Object)val).name}' bones={bones.Length} internal={num3} dead={num} "; object[] obj = new object[4] { num2, num4, text, null }; Bounds bounds = ((Renderer)val).bounds; Vector3 size = ((Bounds)(ref bounds)).size; obj[3] = ((Vector3)(ref size)).ToString("F1"); stringBuilder.Append(text2 + string.Format("external={0} null={1} rootBone={2} bounds={3}", obj)); if (list.Count > 0) { stringBuilder.Append(" externalBones=[" + string.Join(", ", list) + ((num2 > list.Count) ? ", …" : "") + "]"); } } } catch (Exception ex) { stringBuilder.Append(" census threw: " + ex.GetType().Name + ": " + ex.Message); } return stringBuilder.ToString(); } private static bool IsUnder(Transform t, Transform root) { Transform val = t; while ((Object)(object)val != (Object)null) { if ((Object)(object)val == (Object)(object)root) { return true; } val = val.parent; } return false; } private static string PathOf(Transform t) { StringBuilder stringBuilder = new StringBuilder(((Object)t).name); Transform parent = t.parent; while ((Object)(object)parent != (Object)null) { stringBuilder.Insert(0, ((Object)parent).name + "/"); parent = parent.parent; } return stringBuilder.ToString(); } } [HarmonyPatch(typeof(EnvironmentConditions), "RegisterSoulSpot")] internal static class SoulSpotGuard { private static bool Prefix(UID _uid, SoulSpot _soulSpot) { try { if (!EnvironmentConditions.s_soulSpots.ContainsKey(((UID)(ref _uid)).Value)) { return true; } ManualLogSource log = DonorRuntime.Log; if (log != null) { log.LogWarning((object)("[SOULSPOT-GUARD] duplicate soul-spot UID '" + ((UID)(ref _uid)).Value + "' — kept the existing registration instead of throwing mid-Awake (donor-scene collision; the vanilla Add would have crashed the Character's activation).")); } return false; } catch (Exception ex) { ManualLogSource log2 = DonorRuntime.Log; if (log2 != null) { log2.LogWarning((object)("[SOULSPOT-GUARD] prefix failed (" + ex.Message + ") — falling through to vanilla.")); } return true; } } internal static void PruneDead() { try { DictionaryExt s_soulSpots = EnvironmentConditions.s_soulSpots; if (s_soulSpots == null) { return; } List list = new List(); foreach (string key in s_soulSpots.Keys) { if (s_soulSpots[key] != null && (Object)(object)s_soulSpots[key] == (Object)null) { list.Add(key); } } foreach (string item in list) { s_soulSpots.Remove(item); } if (list.Count > 0) { DonorRuntime.Log.LogMessage((object)string.Format("[SOULSPOT-GUARD] pruned {0} dead soul-spot entr{1} from the static registry ({2} live remain).", list.Count, (list.Count == 1) ? "y" : "ies", s_soulSpots.Count)); } } catch (Exception ex) { ManualLogSource log = DonorRuntime.Log; if (log != null) { log.LogWarning((object)("[SOULSPOT-GUARD] prune failed: " + ex.Message)); } } } } public static class StorySense { public static bool IsPrologueActive() { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) try { AreaManager instance = AreaManager.Instance; if ((Object)(object)instance != (Object)null && instance.GetInPrologueScene()) { return true; } Scene activeScene = SceneManager.GetActiveScene(); return string.Equals(((Scene)(ref activeScene)).name, "Tutorial_Standalone", StringComparison.OrdinalIgnoreCase); } catch { return false; } } public static bool CurrentSceneFirstLoad() { try { NetworkLevelLoader instance = NetworkLevelLoader.Instance; return (Object)(object)instance == (Object)null || instance.LevelLoadedForFirstTime; } catch { return true; } } public static bool SceneVisited(string sceneName) { if (string.IsNullOrEmpty(sceneName)) { return false; } try { SaveManager instance = SaveManager.Instance; if (instance == null || string.IsNullOrEmpty(instance.m_worldHostUID)) { return false; } CharacterSaveInstanceHolder val = default(CharacterSaveInstanceHolder); if (!instance.m_charSaves.TryGetValue(instance.m_worldHostUID, ref val) || val == null) { return false; } SaveInstance currentSaveInstance = val.CurrentSaveInstance; if (currentSaveInstance?.PathToSceneSaves == null) { return false; } foreach (string key in currentSaveInstance.PathToSceneSaves.Keys) { if (string.Equals(key, sceneName, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } catch { return false; } } public static bool StoryHeld(out string reason) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) StorySafety val = new StorySafety { InPrologue = IsPrologueActive(), CurrentSceneFirstLoad = CurrentSceneFirstLoad() }; return ExpeditionWarm.StoryHold(ref val, ref reason); } } public sealed class TemplateKind where T : class { private readonly Dictionary _cache = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly List _lru = new List(); private readonly string _holderName; private GameObject _holder; public int RawCount => _cache.Count; public Dictionary Table => _cache; public List LruKeys => _lru; public IEnumerable> Entries => _cache; internal TemplateKind(string holderName) { _holderName = holderName; } public GameObject Holder() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown if ((Object)(object)_holder == (Object)null) { _holder = new GameObject(_holderName); _holder.SetActive(false); Object.DontDestroyOnLoad((Object)(object)_holder); } return _holder; } public bool TryGet(string key, out T entry) { return _cache.TryGetValue(key, out entry); } public void Put(string key, T entry) { _cache[key] = entry; Touch(key); } public bool Remove(string key) { _lru.Remove(key); return _cache.Remove(key); } public void Touch(string key) { _lru.Remove(key); _lru.Add(key); } public int ClearAll() { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown _cache.Clear(); _lru.Clear(); if ((Object)(object)_holder == (Object)null) { return 0; } List list = new List(); foreach (Transform item in _holder.transform) { Transform val = item; list.Add(((Component)val).gameObject); } foreach (GameObject item2 in list) { if ((Object)(object)item2 != (Object)null) { Object.Destroy((Object)(object)item2); } } return list.Count; } } public static class TemplateStore { public static readonly TemplateKind CompanionBodies = new TemplateKind("CK_BodyTemplateHolder"); public static readonly TemplateKind Spawnables = new TemplateKind("SK_TemplateHolder"); public static Action SpawnableNormalizer; public static bool RegisterSpecies(string speciesKey, IEnumerable donorScenes) { return DonorSceneTable.RegisterSpecies(speciesKey, donorScenes); } public static bool RegisterTemplate(string speciesKey, GameObject dormantTemplate) { string text = speciesKey?.Trim() ?? ""; if (text.Length == 0 || (Object)(object)dormantTemplate == (Object)null) { DonorRuntime.Log.LogWarning((object)"[TEMPLATE] RegisterTemplate: species key and a dormant body are both required."); return false; } try { dormantTemplate.transform.SetParent(Spawnables.Holder().transform, false); SpawnableNormalizer?.Invoke(dormantTemplate, text); if (Spawnables.TryGet(text, out var entry) && (Object)(object)entry != (Object)null && entry != dormantTemplate) { Object.Destroy((Object)(object)entry); DonorRuntime.Log.LogMessage((object)("[TEMPLATE] RegisterTemplate('" + text + "'): replacing the existing template — the old dormant body is destroyed.")); } Spawnables.Put(text, dormantTemplate); DonorRuntime.Log.LogMessage((object)("[TEMPLATE] third-party template registered for '" + text + "'" + ((SpawnableNormalizer == null) ? " (stored AS GIVEN — no spawn consumer has wired the normalization pass)" : "") + " — spawns of this key now adopt it with no donor load.")); return true; } catch (Exception arg) { DonorRuntime.Log.LogError((object)$"[TEMPLATE] RegisterTemplate('{text}') threw during normalization — destroying the object: {arg}"); if ((Object)(object)dormantTemplate != (Object)null) { Object.Destroy((Object)(object)dormantTemplate); } if (Spawnables.TryGet(text, out var entry2) && entry2 == dormantTemplate) { Spawnables.Remove(text); } return false; } } } public static class TerrainGuard { public struct Health { public int UnityTerrains; public int MeshTiles; public int BrokenMeshTiles; public int Lod2Missing; } public static Func UnloadModeProvider; public static Func UnloadEveryNProvider; public static Func FlushAfterPurgeProvider; private static UnloadAssetsMode UnloadMode { get { //IL_000e: Unknown result type (might be due to invalid IL or missing references) if (UnloadModeProvider == null) { return (UnloadAssetsMode)1; } return UnloadModeProvider(); } } private static int UnloadEveryN { get { if (UnloadEveryNProvider == null) { return 5; } return UnloadEveryNProvider(); } } private static bool FlushAfterPurge { get { if (FlushAfterPurgeProvider != null) { return FlushAfterPurgeProvider(); } return true; } } public static bool ShouldPurgeThisHarvest(int harvestCount) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return TerrainMaintenance.ShouldUnload(UnloadMode, UnloadEveryN, harvestCount); } public static string UnloadModeDescription() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 //IL_000d: Unknown result type (might be due to invalid IL or missing references) if ((int)UnloadMode != 1) { return $"mode={UnloadMode}"; } return $"mode=EveryN N={UnloadEveryN}"; } public static Health Snapshot(string context) { //IL_009f: 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) Health result = default(Health); try { Terrain[] activeTerrains = Terrain.activeTerrains; result.UnityTerrains = ((activeTerrains != null) ? activeTerrains.Length : 0); if (activeTerrains != null) { Terrain[] array = activeTerrains; foreach (Terrain val in array) { if (!((Object)(object)val == (Object)null)) { TerrainData o = null; Material o2 = null; try { o = val.terrainData; } catch { } try { o2 = val.materialTemplate; } catch { } ManualLogSource log = DonorRuntime.Log; string[] obj3 = new string[9] { "[TERRAIN] (", context, ") unity '", ((Object)val).name, "' scene='", null, null, null, null }; Scene scene = ((Component)val).gameObject.scene; obj3[5] = ((Scene)(ref scene)).name; obj3[6] = "' "; obj3[7] = $"data={FakeNull((Object)(object)o)} material={FakeNull((Object)(object)o2)} basemapDist={val.basemapDistance:F0} "; obj3[8] = $"drawHeightmap={val.drawHeightmap} active={((Component)val).gameObject.activeInHierarchy && ((Behaviour)val).enabled}"; log.LogMessage((object)string.Concat(obj3)); } } } TerrainMeshRenderControl[] array2 = Object.FindObjectsOfType(); result.MeshTiles = ((array2 != null) ? array2.Length : 0); if (array2 != null) { TerrainMeshRenderControl[] array3 = array2; foreach (TerrainMeshRenderControl val2 in array3) { if (!((Object)(object)val2 == (Object)null)) { MeshFilter val3 = ResolveMeshFilter(val2); bool flag = (Object)(object)val3 == (Object)null || (Object)(object)val3.sharedMesh == (Object)null; if (flag) { result.BrokenMeshTiles++; } if (flag && (Object)(object)val2.Lod2 == (Object)null) { result.Lod2Missing++; } } } } DonorRuntime.Log.LogMessage((object)($"[TERRAIN] ({context}) meshTiles={result.MeshTiles} brokenMesh={result.BrokenMeshTiles} " + $"lod2Missing={result.Lod2Missing}" + ((result.BrokenMeshTiles > 0) ? " <-- RENDER HOLE (invisible but WALKABLE tile(s) — the collider is fine). Change zone to fix. Do NOT run terrainfix: it swaps in a coarse mesh at the wrong height (Bug 32)." : ""))); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[TERRAIN] (" + context + ") snapshot failed: " + ex.Message)); } return result; } public static int FlushActiveTerrains(string context) { int num = 0; try { Terrain[] activeTerrains = Terrain.activeTerrains; if (activeTerrains == null) { return 0; } Terrain[] array = activeTerrains; foreach (Terrain val in array) { if (!((Object)(object)val == (Object)null)) { try { val.Flush(); num++; } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[TERRAIN] (" + context + ") Flush('" + ((Object)val).name + "') threw: " + ex.Message)); } } } if (num > 0) { DonorRuntime.Log.LogMessage((object)$"[TERRAIN] ({context}) flushed {num} Unity terrain(s) (basemap/patch re-arm)."); } } catch (Exception ex2) { DonorRuntime.Log.LogWarning((object)("[TERRAIN] (" + context + ") flush failed: " + ex2.Message)); } return num; } public static int RepairMeshTiles(string context) { int num = 0; try { TerrainMeshRenderControl[] array = Object.FindObjectsOfType(); if (array == null) { return 0; } TerrainMeshRenderControl[] array2 = array; foreach (TerrainMeshRenderControl val in array2) { if (!((Object)(object)val == (Object)null)) { MeshFilter val2 = ResolveMeshFilter(val); if (!((Object)(object)val2 == (Object)null) && !((Object)(object)val2.sharedMesh != (Object)null) && !((Object)(object)val.Lod2 == (Object)null)) { val2.sharedMesh = val.Lod2; num++; } } } if (num > 0) { DonorRuntime.Log.LogWarning((object)($"[TERRAIN] ({context}) forced {num} blanked tile(s) to the COARSE Lod2 mesh. " + "The ground is visible again but is drawn at the WRONG HEIGHT (you may appear sunk into it) and the LOD streamer will NOT re-upgrade it — Bug 32. A zone reload is the clean fix. Snapshots will now read brokenMesh=0 for these tiles, which is why that number alone cannot be trusted after a terrainfix.")); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[TERRAIN] (" + context + ") tile repair failed: " + ex.Message)); } return num; } public static void PostHarvestMaintenance(string context, bool purgeRan) { if (purgeRan && FlushAfterPurge) { FlushActiveTerrains(context); } Snapshot(context + (purgeRan ? " post-purge" : " no-purge")); } public static AssetBundle BeginMeshBundleShield(Scene donor, string context) { try { bool flag = false; GameObject[] rootGameObjects = ((Scene)(ref donor)).GetRootGameObjects(); foreach (GameObject val in rootGameObjects) { if ((Object)(object)val.GetComponentInChildren(true) != (Object)null) { flag = true; break; } } if (!flag) { return null; } AssetBundle s_meshAssets = TerrainMeshQualityManager.s_meshAssets; if ((Object)(object)s_meshAssets == (Object)null) { DonorRuntime.Log.LogMessage((object)("[TERRAIN] (" + context + ") donor scene carries a TerrainMeshQualityManager but no live terrain-mesh AssetBundle is loaded — nothing to shield.")); return null; } TerrainMeshQualityManager.s_meshAssets = null; DonorRuntime.Log.LogWarning((object)("[TERRAIN] (" + context + ") donor scene carries its own TerrainMeshQualityManager — SHIELDING the live terrain-mesh AssetBundle across the unload (the donor copy's OnDestroy calls s_meshAssets.Unload(true), destroying every streamed LOD mesh: the V32-b render-hole trigger).")); return s_meshAssets; } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[TERRAIN] (" + context + ") mesh-bundle shield failed to arm: " + ex.Message)); return null; } } public static void EndMeshBundleShield(AssetBundle stashed, string context) { if ((Object)(object)stashed == (Object)null) { return; } try { AssetBundle s_meshAssets = TerrainMeshQualityManager.s_meshAssets; if ((Object)(object)s_meshAssets != (Object)null && s_meshAssets != stashed) { DonorRuntime.Log.LogWarning((object)("[TERRAIN] (" + context + ") a NEW terrain-mesh AssetBundle appeared while the shield was up — keeping the new one (stale stash dropped).")); if ((Object)(object)stashed != (Object)null) { stashed.Unload(true); } } else { TerrainMeshQualityManager.s_meshAssets = stashed; DonorRuntime.Log.LogMessage((object)("[TERRAIN] (" + context + ") terrain-mesh AssetBundle shield removed — live bundle restored intact.")); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[TERRAIN] (" + context + ") mesh-bundle shield failed to disarm: " + ex.Message)); } } public static void Dump(string context) { Snapshot(context); } public static void RepairNow(string context) { FlushActiveTerrains(context); if (RepairMeshTiles(context) == 0) { DonorRuntime.Log.LogMessage((object)("[TERRAIN] (" + context + ") no blanked tiles found (nothing to repair).")); } Snapshot(context + " post-fix"); } private static MeshFilter ResolveMeshFilter(TerrainMeshRenderControl tile) { MeshFilter val = tile.MeshFilter; if ((Object)(object)val == (Object)null) { val = ((Component)tile).GetComponent(); } return val; } private static string FakeNull(Object o) { if (o == null) { return "null"; } if (o == (Object)null) { return "DESTROYED"; } return "OK"; } } }