Decompiled source of DonorKit v0.1.9
plugins/DonorKit.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; 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.AI; using UnityEngine.Events; using UnityEngine.Profiling; 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.9.0")] [assembly: AssemblyInformationalVersion("0.1.9+de8ede7c1cc4e525b56eb73ca1c0bdd9555d1cc3")] [assembly: AssemblyProduct("DonorKit")] [assembly: AssemblyTitle("DonorKit")] [assembly: AssemblyMetadata("BuildStamp", "de8ede7c 2026-09-05")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } } namespace 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<Sounds> _detailed = new HashSet<Sounds>(); 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<SplitableSoundSource> 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) + "' (" + TypeName((Object)(object)instance) + ") 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; } private static string TypeName(Object o) { if (o != null) { return ((object)o).GetType().Name + (Object.op_Implicit(o) ? "" : " <destroyed>"); } return ""; } 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 "<destroyed or null>"; } 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 "<unreadable>"; } } } public sealed class BodyTemplate { public GameObject Dormant; public string Key; public string SpeciesId; public CreatureAttributes Captured; public bool Substituted; public bool NoAiGraph; public string Origin; } public static class BodyTemplateStore { public static Func<Character, object, CreatureAttributes> StatCapturer; private const int CacheWarnThreshold = 24; private static int _lastCensusWarnCount = -1; private static readonly HashSet<string> _warnedSubs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private static readonly HashSet<string> _warnedNoAi = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private const float PruneIntervalSeconds = 5f; private static float _nextPruneAt; private static TemplateKind<BodyTemplate> 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<BodyTemplate>(Kind.Table, speciesId, ref text, ref template, (string)null) || template == null) { return false; } if (DropIfDestroyed(ref template)) { 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 (DropIfDestroyed(ref template)) { return false; } if (template.Substituted) { WarnSubstitutionOnce(speciesId, template.SpeciesId); template = null; return false; } if (template.NoAiGraph) { WarnNoAiGraphOnce(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; } if (!DropIfDestroyed(ref entry)) { return entry; } return null; } private static bool DropIfDestroyed(ref BodyTemplate template) { if (template == null || (Object)(object)template.Dormant != (Object)null) { return false; } foreach (KeyValuePair<string, BodyTemplate> entry in Kind.Entries) { if (entry.Value == template) { Kind.Remove(entry.Key); DonorRuntime.Log.LogWarning((object)("[TEMPLATE] cached body template '" + entry.Key + "' was destroyed — dropped from the cache.")); break; } } template = null; return true; } 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.")); } } private static void WarnNoAiGraphOnce(string asked, string got) { if (_warnedNoAi.Add(asked + "→" + got)) { DonorRuntime.Log.LogWarning((object)("[TEMPLATE] REFUSED: the cached body for '" + asked + "' (species '" + got + "') has NO AI graph — no AIStatesPrefab and no AIRoot child — so anything built from it would stand frozen and untargetable. Treating this as a cache MISS so the caller harvests a fresh donor instead. The body stays resident for fuzzy PET fallback and for 'templatedump' forensics; 'templateclear' drops it.")); } } public static void ResetSubstitutionWarnings() { _warnedSubs.Clear(); _warnedNoAi.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) //IL_031c: Unknown result type (might be due to invalid IL or missing references) //IL_0321: 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<GameObject>(((Component)src).gameObject, Kind.Holder().transform); ((Object)val).name = "CK_BodyTemplate_" + key; val.transform.localPosition = Vector3.zero; PhotonView component = val.GetComponent<PhotonView>(); if ((Object)(object)component != (Object)null) { component.viewID = 0; } AISquadMember[] componentsInChildren = val.GetComponentsInChildren<AISquadMember>(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); CharacterAI component2 = val.GetComponent<CharacterAI>(); bool flag = false; try { flag = (Object)(object)component2 != (Object)null && (Object)(object)component2.AIStatesPrefab != (Object)null; } catch { } int num = 0; AIRoot[] componentsInChildren2 = val.GetComponentsInChildren<AIRoot>(true); foreach (AIRoot val3 in componentsInChildren2) { if (!((Object)(object)val3 == (Object)null)) { num++; } } bool noAiGraph = !flag && num == 0; BodyTemplate bodyTemplate = new BodyTemplate { Dormant = val, Key = key, SpeciesId = text2, Captured = ((StatCapturer != null) ? StatCapturer(src, statHost) : null), Substituted = !StatAdoption.IsSameSpecies(key, text2), NoAiGraph = noAiGraph }; 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 + "'") : "") + (bodyTemplate.NoAiGraph ? " — NO AI GRAPH" : "") + ", stats " + ((bodyTemplate.Captured != null) ? "captured" : "UNREADABLE — config fallback at build") + ").")); if (bodyTemplate.NoAiGraph) { string text3 = "?"; try { Scene scene = ((Component)src).gameObject.scene; text3 = ((Scene)(ref scene)).name; } catch { } string text4 = "?"; try { text4 = src.Name; } catch { } DonorRuntime.Log.LogWarning((object)("[TEMPLATE] '" + key + "' was captured from a donor with NO AI graph — no AIStatesPrefab and no AIRoot child (donor character '" + text4 + "' in scene '" + text3 + "'). That is a decorative / non-combat instance sharing the species name, not the real creature: anything built from it stands frozen and untargetable. A SPAWN of this key will now REFUSE the cache and harvest a fresh donor instead; a pet may still wear it as a fuzzy fallback body. Fix: give this key a donor row whose scene holds the real creature (see DonorScenes.txt).")); } 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; } } public static BodyTemplate RegisterPrebuilt(string key, GameObject source, CreatureAttributes attrs = null, string speciesId = null) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)source == (Object)null || string.IsNullOrEmpty(key)) { DonorRuntime.Log.LogWarning((object)"[TEMPLATE] RegisterPrebuilt: a key and a source body are both required."); return null; } key = key.Trim(); GameObject val = null; try { val = Object.Instantiate<GameObject>(source, Kind.Holder().transform); ((Object)val).name = "CK_BodyTemplate_" + key; val.transform.localPosition = Vector3.zero; PhotonView component = val.GetComponent<PhotonView>(); if ((Object)(object)component != (Object)null) { component.viewID = 0; } AISquadMember[] componentsInChildren = val.GetComponentsInChildren<AISquadMember>(true); foreach (AISquadMember val2 in componentsInChildren) { if ((Object)(object)val2 != (Object)null && (Object)(object)val2.AISquad != (Object)null) { val2.AISquad = null; } } if ((Object)(object)val.GetComponent<Character>() == (Object)null) { val.AddComponent<Character>(); } if ((Object)(object)val.GetComponent<NavMeshAgent>() == (Object)null) { val.AddComponent<NavMeshAgent>(); } BodyTemplate bodyTemplate = new BodyTemplate { Dormant = val, Key = key, SpeciesId = (string.IsNullOrEmpty(speciesId) ? key : speciesId), Captured = attrs, Substituted = false, Origin = "prebuilt" }; 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] prebuilt body registered for '" + key + "' (species '" + bodyTemplate.SpeciesId + "', stats " + ((bodyTemplate.Captured != null) ? "supplied" : "none — config fallback at build") + ") — re-forms of this key now need no scene load.")); return bodyTemplate; } catch (Exception ex) { DonorRuntime.Log.LogError((object)("[TEMPLATE] RegisterPrebuilt('" + 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) { if (SkeletonRig.RunGate(donor, clone, key, ModLog.op_Implicit(DonorRuntime.Log), "puppets from this template") == SkeletonRig.GateOutcome.Unrepairable) { 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 int ClearHarvested(out int kept) { //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_0113: Expected O, but got Unknown _lastCensusWarnCount = -1; HashSet<GameObject> hashSet = new HashSet<GameObject>(); List<string> list = new List<string>(); kept = 0; foreach (KeyValuePair<string, BodyTemplate> entry in Kind.Entries) { if (TemplateClearVerb.ShouldKeep(entry.Value.Origin, false)) { kept++; if ((Object)(object)entry.Value.Dormant != (Object)null) { hashSet.Add(entry.Value.Dormant); } } else { list.Add(entry.Key); } } int num = 0; foreach (string item in list) { if (Kind.Remove(item)) { num++; } } GameObject val = Kind.Holder(); if ((Object)(object)val != (Object)null) { List<GameObject> list2 = new List<GameObject>(); foreach (Transform item2 in val.transform) { Transform val2 = item2; list2.Add(((Component)val2).gameObject); } foreach (GameObject item3 in list2) { if ((Object)(object)item3 != (Object)null && !hashSet.Contains(item3)) { Object.Destroy((Object)(object)item3); } } } return num; } 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<string, BodyTemplate> entry in Kind.Entries) { stringBuilder.Append("\n '" + entry.Key + "' -> species '" + entry.Value.SpeciesId + "' stats=" + ((entry.Value.Captured != null) ? "captured" : "none") + ((entry.Value.Origin != null) ? (" origin=" + entry.Value.Origin) : "") + (entry.Value.Substituted ? (" *** SUBSTITUTE — NOT a '" + entry.Key + "' (spawns refuse it; Bug 31) ***") : "") + (entry.Value.NoAiGraph ? " *** NO AI GRAPH — would stand frozen (spawns refuse it) ***" : "")); } 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<string, BodyTemplate> entry in Kind.Entries) { string text; try { SkinnedMeshRenderer val = (((Object)(object)entry.Value.Dormant != (Object)null) ? entry.Value.Dormant.GetComponentInChildren<SkinnedMeshRenderer>(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() { float unscaledTime = Time.unscaledTime; if (unscaledTime < _nextPruneAt) { return; } _nextPruneAt = unscaledTime + 5f; List<string> list = null; foreach (KeyValuePair<string, BodyTemplate> entry in Kind.Entries) { if ((Object)(object)entry.Value?.Dormant == (Object)null) { List<string> obj = list ?? new List<string>(); 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<bool> RepairSkinnedBones; internal static void Bind(ConfigFile cfg) { RepairSkinnedBones = cfg.Bind<bool>("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<bool> CaptureOnSceneEntry; public static ConfigEntry<string> AutoWarmAtBoot; public static ConfigEntry<string> AlwaysWarmSpecies; public static ConfigEntry<float> AutoWarmRetrySeconds; public static ConfigEntry<float> ReturnRetrySeconds; public static ConfigEntry<bool> AllowCoop; public static ConfigEntry<bool> GuestAutoContinue; internal static void Bind(ConfigFile cfg) { CaptureOnSceneEntry = cfg.Bind<bool>("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 <species>'."); AutoWarmAtBoot = cfg.Bind<string>("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<string>("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<float>("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<float>("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<bool>("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<bool>("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<AmbienceSound>(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<AmbienceSound> list = _timelessField?.GetValue(audioManager) as List<AmbienceSound>; object obj = _startTimesField?.GetValue(audioManager); object obj2 = _endTimesField?.GetValue(audioManager); List<AmbienceZone> list2 = _zonesWaitingField?.GetValue(audioManager) as List<AmbienceZone>; 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<SoundPlayer> 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<AmbienceSound> list = _timelessField?.GetValue(audioManager) as List<AmbienceSound>; List<AmbienceZone> list2 = _zonesWaitingField?.GetValue(audioManager) as List<AmbienceZone>; List<AmbienceZone> originalAmbienceZones = audioManager.m_originalAmbienceZones; List<SoundPlayer> players = GlobalAudioManager.m_players; DonorRuntime.Log.LogMessage((object)("[AMBIENCE-RECON] (" + context + ") timeless=" + Census<AmbienceSound>(list) + " zonesWaiting=" + Census<AmbienceZone>(list2) + " originalZones=" + Census<AmbienceZone>(originalAmbienceZones) + " players=" + Census<SoundPlayer>(players))); LogDeadEntries<AmbienceSound>("timeless", list); LogDeadEntries<AmbienceZone>("zonesWaiting", list2); LogDeadEntries<AmbienceZone>("originalZones", originalAmbienceZones); LogDeadEntries<SoundPlayer>("players", players); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[AMBIENCE-RECON] (" + context + ") audit failed: " + ex.Message)); } } private static string Census<T>(List<T> 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<T>(string label, List<T> 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<Character>(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<Character>(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<Character>(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", ModLog.op_Implicit(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<Camera>(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<MonoBehaviour>(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; private const float AdmissionWaitSeconds = 45f; private static readonly ObjectProbeCadence _probeCadence = new ObjectProbeCadence(); private static readonly HashSet<string> _abandonedScenes = new HashSet<string>(); public static int CyclesThisSession { get; private set; } public static Dictionary<string, List<string>> DonorScenes => DonorSceneTable.DonorScenes; public static Dictionary<string, string> DonorPins => DonorSceneTable.DonorPins; private static void LogCycleCensus() { try { int num = -1; int num2 = -1; int num3 = -1; float num4 = -1f; if (_probeCadence.ShouldProbe(CyclesThisSession)) { float realtimeSinceStartup = Time.realtimeSinceStartup; num = Resources.FindObjectsOfTypeAll<Mesh>().Length; num2 = Resources.FindObjectsOfTypeAll<Texture>().Length; num3 = Resources.FindObjectsOfTypeAll<Material>().Length; num4 = (Time.realtimeSinceStartup - realtimeSinceStartup) * 1000f; _probeCadence.Observe(num4); } DonorRuntime.Log.LogMessage((object)ResourceCensus.FormatMem("harvest-cycle", CyclesThisSession, GC.GetTotalMemory(forceFullCollection: false), Profiler.GetTotalAllocatedMemoryLong(), Profiler.GetTotalReservedMemoryLong(), num, num2, num3, num4, Time.realtimeSinceStartup)); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[MEM] cycle census failed: " + ex.GetType().Name + ": " + ex.Message + " — harvest unaffected.")); } } 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<string> sceneNames, out string searchTerm) { return DonorSceneTable.TryGetDonorScenes(speciesId, out sceneNames, out searchTerm); } public static bool TryGetExpeditionScenes(string speciesId, out List<string> sceneNames, out string searchTerm) { return DonorSceneTable.TryGetExpeditionScenes(speciesId, out sceneNames, out searchTerm); } public static string ResolveBuildScene(string want, out List<string> 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<string> sceneNames, string creatureName, Func<Character, object> use, Action<object> onResult) { return HarvestChain(sceneNames, creatureName, use, onResult, 0); } public static IEnumerator HarvestChain(List<string> sceneNames, string creatureName, Func<Character, object> use, Action<object> onResult, int maxCandidates = 0) { object result = null; string fallbackScene = null; int limit = ((maxCandidates > 0) ? Math.Min(maxCandidates, sceneNames.Count) : sceneNames.Count); if (limit < sceneNames.Count) { DonorRuntime.Log.LogMessage((object)$"[HARVEST] chain for '{creatureName}' capped at {limit} of {sceneNames.Count} candidate scene(s) by the caller."); } for (int i = 0; i < limit; i++) { if (result != null) { break; } if (sceneNames.Count > 1) { DonorRuntime.Log.LogMessage((object)$"[HARVEST] chain {i + 1}/{limit} for '{creatureName}': '{sceneNames[i]}'."); } bool acceptSubstring = DonorChain.AcceptSubstringAt(i, limit, 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 + "'" + ((limit < sceneNames.Count) ? $" within the caller's {limit}-scene cap." : "."))); } onResult(result); } private static IEnumerator HarvestRanked(string sceneName, string creatureName, Func<Character, object> use, bool acceptSubstring, Action<object, int> 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 + "'.")); } ModLog 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<Character, object> use, Action<object> 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; } ModLog 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<Scene, object> useScene, Action<object> onResult) { object result = null; yield return HarvestSceneCore(sceneName, label, useScene, delegate(object r) { result = r; }); onResult(result); } private static IEnumerator HarvestSceneCore(string sceneName, string label, Func<Scene, object> useScene, Action<object> report) { DonorRuntime.Log.LogMessage((object)("[HARVEST] start: scene='" + sceneName + "' " + label)); float t0 = Time.realtimeSinceStartup; if (_abandonedScenes.Contains(sceneName)) { DonorRuntime.Log.LogWarning((object)("[HARVEST] REFUSING donor scene '" + sceneName + "' — its additive load hard-abandoned earlier this session and is quarantined until relaunch (the wedged copy may still be loading; species with another donor in their chain fall through to it).")); yield break; } 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).")); yield break; } if (!HarvestAdmission.SafeToStart(out var why)) { DonorRuntime.Log.LogMessage((object)$"[HARVEST] '{sceneName}' holding — {why} (waiting up to {45f:F0}s for the world to settle)."); float waitUntil = Time.realtimeSinceStartup + 45f; while (!HarvestAdmission.SafeToStart(out why)) { if (Time.realtimeSinceStartup >= waitUntil) { DonorRuntime.Log.LogWarning((object)($"[HARVEST] REFUSING '{sceneName}' — {why} for {45f:F0}s. " + "(Unattended callers re-queue; a pet re-form retries on the next player-ready pass; a dev verb should be re-run.)")); yield break; } yield return null; } DonorRuntime.Log.LogMessage((object)("[HARVEST] '" + sceneName + "' proceeding — the world settled.")); } if (DonorPhotonGuard.WindowActive) { DonorRuntime.Log.LogMessage((object)("[HARVEST] another harvest's guard window is open — '" + sceneName + "' waiting its turn (harvests are serialized).")); float waitUntil = Time.realtimeSinceStartup; while (DonorPhotonGuard.WindowActive) { yield return null; } DonorRuntime.Log.LogMessage((object)$"[HARVEST] '{sceneName}' proceeding after {Time.realtimeSinceStartup - waitUntil:F2}s wait."); if (!HarvestAdmission.SafeToStart(out var why2)) { DonorRuntime.Log.LogWarning((object)("[HARVEST] REFUSING '" + sceneName + "' — " + why2 + " (the world changed while we waited our turn).")); yield break; } } 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; TerrainManagerGuard.CaptureReal(); Material realSkybox = RenderSettings.skybox; try { SkySnapshot.Log("pre-harvest '" + sceneName + "'", ModLog.op_Implicit(DonorRuntime.Log)); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[SKY] pre-harvest snapshot threw (" + ex.GetType().Name + ": " + ex.Message + ") — continuing.")); } float[] realWaterGlobals = DonorDisplayRepair.CaptureWaterGlobals(); int frozenAtLoad = 0; bool abandoned = false; UnityAction<Scene, LoadSceneMode> freezeOnLoad = null; 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 //IL_00fe: Unknown result type (might be due to invalid IL or missing references) if ((int)m != 1) { if (abandoned) { SceneManager.sceneLoaded -= freezeOnLoad; DonorRuntime.Log.LogMessage((object)("[HARVEST] dropped the retained freeze hook for abandoned '" + sceneName + "' — a non-additive load ('" + ((Scene)(ref s)).name + "') took its half-loaded copy with it; nothing left to catch.")); } } else if (!(((Scene)(ref s)).name != sceneName)) { GameObject[] rootGameObjects4 = ((Scene)(ref s)).GetRootGameObjects(); foreach (GameObject val5 in rootGameObjects4) { DonorDisplayRepair.NeutralizeCamerasUnder(val5, "sceneLoaded"); val5.SetActive(false); frozenAtLoad++; } if (abandoned) { SceneManager.sceneLoaded -= freezeOnLoad; DonorRuntime.Log.LogWarning((object)($"[HARVEST] ABANDONED donor scene '{sceneName}' completed LATE — froze {frozenAtLoad} " + "root(s) so it can never run Start/Update; scheduling its unload. NB its activation ran OUTSIDE the guard window: if audio/skybox/terrain look wrong from here, this line is the attribution.")); if ((Object)(object)DonorRuntime.Runner != (Object)null) { DonorRuntime.Runner.StartCoroutine(UnloadAbandoned(s)); } else { DonorRuntime.Log.LogWarning((object)("[HARVEST] no coroutine runner — the frozen abandoned copy of '" + sceneName + "' stays resident.")); } } } }; bool restoresRan = false; object result = null; AssetBundle meshBundleShield = null; try { SceneManager.sceneLoaded += freezeOnLoad; DonorDisplayRepair.ArmCameraSentinel(sceneName); float loadStart = Time.realtimeSinceStartup; AsyncOperation load; try { load = SceneManager.LoadSceneAsync(sceneName, (LoadSceneMode)1); } catch (Exception ex2) { DonorRuntime.Log.LogWarning((object)("[HARVEST] LoadSceneAsync threw: " + ex2.Message)); yield break; } if (load == null) { DonorRuntime.Log.LogWarning((object)("[HARVEST] LoadSceneAsync returned null — bad scene name '" + sceneName + "'?")); yield break; } float waitUntil = Time.realtimeSinceStartup + 30f; float abandonAt = Time.realtimeSinceStartup + 30f + 120f; while (!load.isDone) { DonorPhotonGuard.Refresh(guardToken); if (waitUntil > 0f && Time.realtimeSinceStartup >= waitUntil) { 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)."); waitUntil = -1f; } if (Time.realtimeSinceStartup >= abandonAt) { abandoned = true; _abandonedScenes.Add(sceneName); DonorRuntime.Log.LogWarning((object)("[HARVEST] ABANDONING donor scene '" + sceneName + "' — load STILL not done after " + $"{150f:F0}s (truly wedged). Consequences: the guard window closes " + "and singletons restore NOW (so saves + later harvests stop starving); '" + sceneName + "' is QUARANTINED for the rest of the session (later harvests of it refuse loudly — chain species fall through to their next donor); the sceneLoaded freeze hook stays ARMED so a late completion is frozen, attributed and unloaded instead of activating unguarded.")); 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.")); yield break; } DonorRuntime.Log.LogMessage((object)$"[HARVEST] donor scene loaded in {Time.realtimeSinceStartup - loadStart:F2}s (waited={loadStart - t0:F2}s loaded={Time.realtimeSinceStartup - loadStart:F2}s, {((Scene)(ref sceneByName)).rootCount} roots)."); int num = 0; GameObject[] rootGameObjects = ((Scene)(ref sceneByName)).GetRootGameObjects(); foreach (GameObject val in rootGameObjects) { num += val.GetComponentsInChildren<PhotonView>(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<Camera>(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<CameraQuality>() != (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 ex3) { DonorRuntime.Log.LogWarning((object)("[HARVEST] processing threw: " + ex3.Message + "\n" + ex3.StackTrace)); } float u0 = Time.realtimeSinceStartup; DonorPhotonGuard.Refresh(guardToken); meshBundleShield = TerrainGuard.BeginMeshBundleShield(sceneByName, "pre-unload"); AsyncOperation unload = SceneManager.UnloadSceneAsync(sceneByName); while (unload != null && !unload.isDone) { DonorPhotonGuard.Refresh(guardToken); yield return null; } 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})."); LogCycleCensus(); 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 ex4) { DonorRuntime.Log.LogWarning((object)("[PROBE-FIX] Tetrahedralize failed: " + ex4.Message)); } DonorDisplayRepair.RestoreSkybox(realSkybox, "post-unload"); DonorDisplayRepair.RepairDisplayChain("post-unload"); DonorDisplayRepair.CheckWaterGlobals(realWaterGlobals, "post-unload", restore: true); SkySnapshot.Log("post-unload", ModLog.op_Implicit(DonorRuntime.Log)); if ((Object)(object)DonorRuntime.Runner != (Object)null) { DonorRuntime.Runner.StartCoroutine(DonorDisplayRepair.DeferredSkyCheck(realSkybox, realWaterGlobals)); } } finally { if (!abandoned) { SceneManager.sceneLoaded -= freezeOnLoad; } DonorDisplayRepair.DisarmCameraSentinel(); TerrainGuard.EndMeshBundleShield(meshBundleShield, "finally"); RunSingletonRestores(); DonorPhotonGuard.End(guardToken, abandoned); } 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(); ItemUidGuard.LogSummary(); 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")); report(result); void RunSingletonRestores() { if (!restoresRan) { restoresRan = true; DonorSingletonRestore.RestoreAudioManager(realAudio); DonorSingletonRestore.RestoreCombatManager(realCombat); DonorSingletonRestore.RestoreEnvironment(realEnv); DonorSingletonRestore.RestoreSquadManager(realSquads); DonorSingletonRestore.RestoreTerrainMeshManager(realTerrainMesh); TerrainManagerGuard.RestoreReal(); } } } private static IEnumerator UnloadAbandoned(Scene scene) { //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) if (!((Scene)(ref scene)).IsValid() || !((Scene)(ref scene)).isLoaded) { yield break; } string name = ((Scene)(ref scene)).name; if (DonorPhotonGuard.WindowActive) { DonorRuntime.Log.LogMessage((object)("[HARVEST] late unload of abandoned '" + name + "' waiting for the open guard window (harvests are serialized).")); while (DonorPhotonGuard.WindowActive) { yield return null; } } int guardToken = DonorPhotonGuard.Begin(name); try { AsyncOperation unload = null; try { unload = SceneManager.UnloadSceneAsync(scene); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[HARVEST] late unload of abandoned '" + name + "' threw: " + ex.Message + " — frozen copy stays resident.")); } while (unload != null && !unload.isDone) { DonorPhotonGuard.Refresh(guardToken); yield return null; } DonorRuntime.Log.LogMessage((object)("[HARVEST] late-completed abandoned donor '" + name + "' unloaded (scene stays quarantined this session).")); } finally { DonorPhotonGuard.End(guardToken, abandonedByCaller: false); } } } [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 _saveDeferred; 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 SaveDeferralOutstanding => _saveDeferred; 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) { End(token, abandonedByCaller: false); } public static void End(int token, bool abandonedByCaller) { //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; } bool flag = (double)Time.realtimeSinceStartup >= _window.ExpireAt; string text = (flag ? " (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; ReplayDeferredSave(abandonedByCaller, flag); } } public static void NoteDeferredSave() { _saveDeferred = true; } private static void ReplayDeferredSave(bool abandonedByCaller, bool deadlineLapsed) { if (!_saveDeferred) { return; } bool flag = ReplayPolicy.ShouldReplay(_saveDeferred, abandonedByCaller, deadlineLapsed); _saveDeferred = false; if (!flag) { DonorRuntime.Log.LogWarning((object)("[SAVE-GUARD] not replaying the deferred save now — " + ReplayPolicy.WithheldReason(abandonedByCaller, deadlineLapsed) + "; the donor scene may be half-loaded, so m_saveRequired is left armed for vanilla's own replay.")); return; } try { SaveManager instance = SaveManager.Instance; if (instance != null) { bool saveRequired = instance.m_saveRequired; instance.m_saveRequired = false; float lastSaveTime = instance.m_lastSaveTime; instance.Save(); if (ReplayVerdict.Classify(instance.m_savingPending, instance.m_lastSaveTime != lastSaveTime) != ReplayOutcome.Ran && saveRequired) { DonorRuntime.Log.LogWarning((object)("[SAVE-GUARD] replay REFUSED by vanilla — Save() returned without stamping m_lastSaveTime (" + RefusalReason(instance) + "). NOT re-arming m_saveRequired (a latched flag reads as a permanent save-in-progress); the next autosave covers it.")); } } } catch (Exception ex) { _saveDeferred = true; DonorRuntime.Log.LogWarning((object)("[SAVE-GUARD] deferred-save replay THREW — the deferral is re-armed for the next window close (it would otherwise have been lost): " + ex)); } } private static string RefusalReason(SaveManager sm) { //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) try { NetworkLevelLoader instance = NetworkLevelLoader.Instance; if ((Object)(object)instance != (Object)null && instance.InLoading) { return "an area load is in flight"; } if ((Object)(object)instance != (Object)null && instance.IsJoiningWorld) { return "joining a world"; } if (sm != null && sm.SavesRestricted) { return "saves are restricted (SavesRestricted)"; } Scene activeScene = SceneManager.GetActiveScene(); string name = ((Scene)(ref activeScene)).name; if (name == "Tutorial_Standalone") { return "the standalone tutorial never saves"; } if ((Object)(object)CharacterManager.Instance != (Object)null && CharacterManager.Instance.HardcoreDeathTriggered) { return "a hardcore death is being processed — vanilla DELETES the local saves rather than writing one"; } return "none of Save()'s four early-return conditions reads true — cause unknown"; } catch (Exception ex) { return "state read threw: " + ex.Message; } } 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_00ef: 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) //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) //IL_0146: Unknown result type (might be due to invalid IL or missing references) //IL_0225: Unknown result type (might be due to invalid IL or missing references) //IL_022a: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) //IL_01c0: Unknown result type (might be due to invalid IL or missing references) //IL_01c5: 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.LogInfo((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 val; if (!((Object)(object)value != (Object)null)) { obj = "<destroyed>"; } else { string[] obj2 = new string[5] { "'", ((Object)((Component)value).gameObject).name, "' (scene '", null, null }; val = ((Component)value).gameObject.scene; obj2[3] = ((Scene)(ref val)).name; obj2[4] = "')"; obj = string.Concat(obj2); } string text = (string)obj; val = SceneManager.GetActiveScene(); string name = ((Scene)(ref val)).name; bool flag = (Object)(object)value != (Object)null && (Object)(object)((Component)value).gameObject != (Object)null; int viewID = netView.viewID; object obj3; if (!flag) { obj3 = null; } else { val = ((Component)value).gameObject.scene; obj3 = ((Scene)(ref val)).name; } val = ((Component)netView).gameObject.scene; if (DuplicateViewPolicy.ShouldVeto(viewID, flag, (string)obj3, ((Scene)(ref val)).name, name)) { int viewID2 = netView.viewID; netView.removedFromLocalViewList = true; if (viewID2 != 0) { netView.viewID = 0; } netView.synchronization = (ViewSynchronization)0; ModLog log = DonorRuntime.Log; string[] obj4 = new string[8] { $"[PHOTON-GUARD] VETOED duplicate registration of scene-baked view id {viewID2} ", "by out-of-scene newcomer '", ((Object)((Component)netView).gameObject).name, "' (scene '", null, null, null, null }; val = ((Component)netView).gameObject.scene; obj4[4] = ((Scene)(ref val)).name; obj4[5] = "') — vanilla would have destroyed old="; obj4[6] = text; obj4[7] = ", orphaning the id on this machine forever (phantom view-ID spam class). Newcomer neutralized (id 0, sync Off, OnDestroy defanged)."; log.LogWarning((object)string.Concat(obj4)); return false; } ModLog log2 = DonorRuntime.Log; object[] obj5 = new object[4] { netView.viewID, ((Object)((Component)netView).gameObject).name, null, null }; val = ((Component)netView).gameObject.scene; obj5[2] = ((Scene)(ref val)).name; obj5[3] = text; log2.LogMessage((object)string.Format("[PHOTON-RECON] duplicate view id {0}: new='{1}' (scene '{2}') will destroy old={3}.", obj5)); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[PHOTON-GUARD] prefix error (registration allowed): " + ex.Message)); } return true; } public static void DumpRegistry() { ViewRegistryDump.Dump(ModLog.op_Implicit(DonorRuntime.Log)); } } public static class DonorRuntime { public static ModLog 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; } SaveGuardState s = ReadState(_async); string why; SaveGuardAction saveGuardAction = SaveGuardPolicy.Decide(s, out why); if (saveGuardAction == SaveGuardAction.Defer) { __instance.SetSaveRequired(); DonorPhotonGuard.NoteDeferredSave(); DonorRuntime.Log.LogWarning((object)$"[SAVE-GUARD] DEFERRED save (async={_async}, forceEnv={_forceSaveEnvironment}) — {why}; replayed when the harvest window closes."); return false; } DonorRuntime.Log.LogWarning((object)($"[SAVE-GUARD] ALLOWED save mid-harvest (async={_async}, forceEnv={_forceSaveEnvironment}) — {why}. " + "Deferring this one would hang or drop it; a mid-window snapshot is the lesser evil.")); return true; } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[SAVE-GUARD] prefix error (save allowed): " + ex.Message)); return true; } } internal static SaveGuardState ReadState(bool async) { SaveGuardState result = new SaveGuardState { WindowActive = true, Async = async }; try { result.AppClosing = Global.IsApplicationClosing; } catch { result.AppClosing = true; } try { NetworkLevelLoader instance = NetworkLevelLoader.Instance; if ((Object)(object)instance == (Object)null) { result.OverallLoadingDone = false; return result; } result.OverallLoadingDone = instance.IsOverallLoadingDone; result.WaitingForOtherPlayers = instance.m_waitingForOtherPlayers; result.JoiningWorld = instance.IsJoiningWorld; result.PrepareJoining = instance.m_prepareJoining; result.InLoading = instance.InLoading; result.PrepingLoadLevel = instance.m_prepingLoadLevel; } catch { result.OverallLoadingDone = false; } try { result.ReturningToMainMenu = (Object)(object)MenuManager.Instance != (Object)null && MenuManager.Instance.IsReturningToMainMenu; } catch { result.ReturningToMainMenu = true; } return result; } internal static string Describe(SaveGuardState s) { return $"appClosing={s.AppClosing} overallDone={s.OverallLoadingDone} " + $"waitingOthers={s.WaitingForOtherPlayers} joining={s.JoiningWorld} prepareJoining={s.PrepareJoining} " + $"inLoading={s.InLoading} preping={s.PrepingLoadLevel} toMainMenu={s.ReturningToMainMenu}"; } } internal static class DonorSceneTable { private static Dictionary<string, List<string>> _donorScenes; private static Dictionary<string, string> _donorPins; internal static Dictionary<string, List<string>> DonorScenes { get { if (_donorScenes == null) { LoadDonorScenes(); } return _donorScenes; } } internal static Dictionary<string, string> 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<string, List<string>> dictionary = DonorTable.Parse(text); Dictionary<string, string> 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<string, List<string>> 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<string> donorScenes) { string text = speciesKey?.Trim() ?? ""; List<string> list = new List<string>(); 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<string, List<string>> donorScenes2 = DonorScenes; if (!donorScenes2.TryGetValue(text, out var value) || value == null) { value = (donorScenes2[text] = new List<string>()); } 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<string> 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<string> list = default(List<string>); if (!SpeciesTable.TryResolveKey<List<string>>(DonorScenes, speciesId, ref searchTerm, ref list, (string)null) || list == null || list.Count == 0) { return false; } List<string> list3 = default(List<string>); List<string> list2 = DonorTable.FilterViable((IEnumerable<string>)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<string>)list2, ((Scene)(ref activeScene)).name); return true; } internal static bool TryGetExpeditionScenes(string speciesId, out List<string> sceneNames, out string searchTerm) { sceneNames = null; List<string> list = default(List<string>); if (!SpeciesTable.TryResolveKey<List<string>>(DonorScenes, speciesId, ref searchTerm, ref list, (string)null) || list == null || list.Count == 0) { return false; } List<string> list2 = DonorTable.ExpeditionCandidates((IEnumerable<string>)list); if (list2.Count == 0) { return false; } sceneNames = list2; return true; } internal static string ResolveBuildScene(string want, out List<string> similar) { return BuildScenes.Resolve(want, ref similar); } internal static void DumpBuildScenes() { BuildScenes.Dump(ModLog.op_Implicit(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; ModLog 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; ModLog 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<EnvironmentConditions>(); if (!((Object)(object)val == (Object)null)) { EnvironmentConditions.Instance = val; ModLog log = DonorRuntime.Log; scene = ((Co