Decompiled source of DonorKit v0.1.1
DonorKit/DonorKit.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using CompanionKit.Core; using ExitGames.Client.Photon; using ForgeKit; using HarmonyLib; using Microsoft.CodeAnalysis; using NetKit; using NetKit.Core; using UnityEngine; using UnityEngine.Events; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("DonorKit")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.1.0")] [assembly: AssemblyInformationalVersion("0.1.1+c48b2eb6460f1b993d9ddb914cbdc5ff6f1d22c6")] [assembly: AssemblyProduct("DonorKit")] [assembly: AssemblyTitle("DonorKit")] [assembly: AssemblyMetadata("BuildStamp", "c48b2eb 2026-07-30")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.1.0")] [module: UnverifiableCode] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } } namespace DonorKit { [HarmonyPatch(typeof(AmbienceSound), "OnUpdate")] public static class AmbienceSoundGuard { [HarmonyFinalizer] private static Exception Finalizer(Exception __exception, AmbienceSound __instance) { return AmbienceGuard.Handle(__exception, __instance); } } [HarmonyPatch(typeof(AmbienceZone), "OnUpdate")] public static class AmbienceZoneGuard { [HarmonyFinalizer] private static Exception Finalizer(Exception __exception, AmbienceZone __instance) { return AmbienceGuard.Handle(__exception, (AmbienceSound)(object)__instance); } } [HarmonyPatch(typeof(GlobalAudioManager), "ClearPlayers")] public static class ClearPlayersGuard { [HarmonyFinalizer] private static Exception Finalizer(Exception __exception) { if (__exception == null) { return null; } DonorRuntime.Log.LogWarning((object)("[AMBIENCE-GUARD] GlobalAudioManager.ClearPlayers threw (" + __exception.GetType().Name + ": " + __exception.Message + ") — swallowing so it can't abort a scene load/area switch.")); return null; } } [HarmonyPatch(typeof(GlobalAudioManager), "GetUsableSoundInstance")] public static class GetUsableSoundInstanceGuard { private static readonly HashSet<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) + "' (" + ((object)instance)?.GetType().Name + ") OnUpdate threw (" + exception.GetType().Name + ": " + exception.Message + ") — disabling it so it stops retrying every frame.")); try { if ((Object)(object)instance != (Object)null) { ((Behaviour)instance).enabled = false; } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[AMBIENCE-GUARD] disable failed: " + ex.Message)); } try { if ((Object)(object)instance != (Object)null && DonorHarvest.RemoveFromAudioRegistries(instance)) { DonorRuntime.Log.LogWarning((object)("[AMBIENCE-GUARD] '" + PathOf((Component)(object)instance) + "' also unregistered from GlobalAudioManager — manager-driven .Play() calls would otherwise keep retrying it after the disable.")); } } catch (Exception ex2) { DonorRuntime.Log.LogWarning((object)("[AMBIENCE-GUARD] unregister failed: " + ex2.Message)); } return null; } public static string PathOf(Component c) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)c == (Object)null) { return "<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 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 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 (!StatAdoption.IsSameSpecies(speciesId, template.SpeciesId)) { WarnSubstitutionOnce(speciesId, template.SpeciesId); } return true; } public static bool TryResolveExact(string speciesId, out BodyTemplate template) { Prune(); template = null; if (string.IsNullOrEmpty(speciesId)) { return false; } if (!Kind.TryGet(speciesId.Trim(), out template) || template == null) { return false; } if (template.Substituted) { WarnSubstitutionOnce(speciesId, template.SpeciesId); template = null; return false; } return true; } public static BodyTemplate GetResident(string key) { Prune(); if (string.IsNullOrEmpty(key)) { return null; } if (!Kind.TryGet(key.Trim(), out var entry)) { return null; } return entry; } private static void WarnSubstitutionOnce(string asked, string got) { if (_warnedSubs.Add(asked + "→" + got)) { DonorRuntime.Log.LogWarning((object)("[TEMPLATE] SUBSTITUTION: asked for '" + asked + "', but the cached body is a '" + got + "' — NOT the same creature (different stats, possibly a different model; e.g. Elite Gargoyle Mage 1700hp vs Gargoyle 750hp). A PET may still wear it as a fallback body, but it will never be recorded as its identity (Bug 31). A SPAWN refuses it outright. If '" + asked + "' matters, it needs its own donor row + a capture; see Bug 28.")); } } public static void ResetSubstitutionWarnings() { _warnedSubs.Clear(); } public static BodyTemplate Capture(Character src, string key, object statHost = null) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)src == (Object)null || string.IsNullOrEmpty(key)) { return null; } GameObject val = null; try { val = Object.Instantiate<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); BodyTemplate bodyTemplate = new BodyTemplate { Dormant = val, Key = key, SpeciesId = text2, Captured = ((StatCapturer != null) ? StatCapturer(src, statHost) : null), Substituted = !StatAdoption.IsSameSpecies(key, text2) }; if (Kind.TryGet(key, out var entry) && (Object)(object)entry?.Dormant != (Object)null && (Object)(object)entry.Dormant != (Object)(object)val) { Object.Destroy((Object)(object)entry.Dormant); } Kind.Put(key, bodyTemplate); DonorRuntime.Log.LogMessage((object)("[TEMPLATE] cached body template '" + key + "' (species '" + bodyTemplate.SpeciesId + "'" + (bodyTemplate.Substituted ? (" — SUBSTITUTE: this is NOT a '" + key + "'") : "") + ", stats " + ((bodyTemplate.Captured != null) ? "captured" : "UNREADABLE — config fallback at build") + ").")); if (bodyTemplate.Substituted) { DonorRuntime.Log.LogWarning((object)("[TEMPLATE] '" + key + "' was captured from a '" + text2 + "' donor — the two are different creatures. A spawn of this key will now REFUSE the cache and harvest honestly; a pet may still wear it as a fallback body but will never record it as its identity (Bug 31). Fix: give '" + key + "' its own donor row, or pin it (Bug 28).")); } if (Kind.RawCount > 0 && Kind.RawCount % 24 == 0 && Kind.RawCount != _lastCensusWarnCount) { _lastCensusWarnCount = Kind.RawCount; DonorRuntime.Log.LogWarning((object)($"[TEMPLATE] cache now holds {Kind.RawCount} resident body template(s) — " + "each pins its mesh/texture assets in memory. 'templateclear' frees them (safe when no re-form is mid-build).")); } return bodyTemplate; } catch (Exception ex) { DonorRuntime.Log.LogError((object)("[TEMPLATE] capture of '" + key + "' threw — destroying the orphan clone: " + ex.Message)); if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } return null; } } private static void RigCheck(GameObject donor, GameObject clone, string key) { Action<string> warn = delegate(string w) { DonorRuntime.Log.LogWarning((object)("[RIG] ('" + key + "') " + w)); }; SkeletonRig.Report report = SkeletonRig.Audit(donor, clone, warn); DonorRuntime.Log.LogMessage((object)("[TEMPLATE] rig ('" + key + "'): " + report.Summary + ".")); if (report.Healthy) { return; } SkeletonRig.LogForensics(report, donor, warn); if (!SkeletonRig.RepairEnabled) { DonorRuntime.Log.LogWarning((object)("[TEMPLATE] rig ('" + key + "'): external references present and [Rig] RepairSkinnedBones=false (log-only) — puppets from this template will mis-render.")); return; } string text = SkeletonRig.Repair(donor, clone, report, warn); SkeletonRig.Report report2 = SkeletonRig.Audit(null, clone, warn); DonorRuntime.Log.LogMessage((object)("[TEMPLATE] rig repair ('" + key + "'): " + text + " — post: " + report2.Summary + ".")); if (!report2.Healthy) { DonorRuntime.Log.LogWarning((object)("[TEMPLATE] rig ('" + key + "'): still unhealthy after the repair pass — keeping the template anyway (no chain to advance here; a mis-skinned body beats none).")); } } public static int Clear() { _lastCensusWarnCount = -1; return Kind.ClearAll(); } public static string Dump() { Prune(); if (Kind.RawCount == 0) { return "[TEMPLATE] body-template cache empty."; } StringBuilder stringBuilder = new StringBuilder($"[TEMPLATE] {Kind.RawCount} cached body template(s):"); foreach (KeyValuePair<string, BodyTemplate> entry in Kind.Entries) { stringBuilder.Append("\n '" + entry.Key + "' -> species '" + entry.Value.SpeciesId + "' stats=" + ((entry.Value.Captured != null) ? "captured" : "none") + (entry.Value.Substituted ? (" *** SUBSTITUTE — NOT a '" + entry.Key + "' (spawns refuse it; Bug 31) ***") : "")); } return stringBuilder.ToString(); } public static string Probe() { Prune(); if (Kind.RawCount == 0) { return "[TEMPLATE] body-template cache empty — nothing to probe."; } StringBuilder stringBuilder = new StringBuilder($"[TEMPLATE] mesh probe over {Kind.RawCount} cached template(s):"); foreach (KeyValuePair<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() { 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", 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; public static int CyclesThisSession { get; private set; } public static Dictionary<string, List<string>> DonorScenes => DonorSceneTable.DonorScenes; public static Dictionary<string, string> DonorPins => DonorSceneTable.DonorPins; public static string PinFor(string tableKey) { return DonorSceneTable.PinFor(tableKey); } public static string IdentityFor(string tableKey, Character donor) { return DonorSceneTable.IdentityFor(tableKey, donor); } public static bool TryGetDonorScenes(string speciesId, out List<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) { object result = null; string fallbackScene = null; for (int i = 0; i < sceneNames.Count; i++) { if (result != null) { break; } if (sceneNames.Count > 1) { DonorRuntime.Log.LogMessage((object)$"[HARVEST] chain {i + 1}/{sceneNames.Count} for '{creatureName}': '{sceneNames[i]}'."); } bool acceptSubstring = DonorChain.AcceptSubstringAt(i, sceneNames.Count, fallbackScene != null); object r = null; int rank = 0; yield return HarvestRanked(sceneNames[i], creatureName, use, acceptSubstring, delegate(object res, int rk) { r = res; rank = rk; }); if (r != null) { result = r; break; } if (rank == 1 && fallbackScene == null) { fallbackScene = sceneNames[i]; } } if (result == null && fallbackScene != null) { DonorRuntime.Log.LogMessage((object)("[HARVEST] no exact-name '" + creatureName + "' donor in the chain — falling back to the substring donor in '" + fallbackScene + "'.")); object r2 = null; yield return HarvestRanked(fallbackScene, creatureName, use, acceptSubstring: true, delegate(object res, int rk) { r2 = res; }); result = r2; } if (result == null && sceneNames.Count > 1) { DonorRuntime.Log.LogWarning((object)("[HARVEST] chain exhausted — no candidate yielded a live '" + creatureName + "'.")); } onResult(result); } private static IEnumerator HarvestRanked(string sceneName, string creatureName, Func<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 + "'.")); } ManualLogSource log = DonorRuntime.Log; string[] obj = new string[5] { "[HARVEST] found donor '", val.Name, "' at ", null, null }; Vector3 position = ((Component)val).transform.position; obj[3] = ((Vector3)(ref position)).ToString("F1"); obj[4] = " — running payload."; log.LogMessage((object)string.Concat(obj)); return use(val); }, delegate(object r) { onResult.Invoke(r, foundRank); }); } public static IEnumerator Harvest(string sceneName, string creatureName, Func<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; } ManualLogSource log = DonorRuntime.Log; string[] obj = new string[5] { "[HARVEST] found donor '", val.Name, "' at ", null, null }; Vector3 position = ((Component)val).transform.position; obj[3] = ((Vector3)(ref position)).ToString("F1"); obj[4] = " — running payload."; log.LogMessage((object)string.Concat(obj)); return use(val); }, onResult); } public static IEnumerator HarvestScene(string sceneName, string label, Func<Scene, object> useScene, Action<object> onResult) { DonorRuntime.Log.LogMessage((object)("[HARVEST] start: scene='" + sceneName + "' " + label)); float t0 = Time.realtimeSinceStartup; Scene activeScene = SceneManager.GetActiveScene(); if (((Scene)(ref activeScene)).name == sceneName) { DonorRuntime.Log.LogWarning((object)("[HARVEST] donor scene '" + sceneName + "' IS the current scene — refusing (re-form from a wild one instead).")); onResult(null); yield break; } if (DonorPhotonGuard.WindowActive) { DonorRuntime.Log.LogMessage((object)("[HARVEST] another harvest's guard window is open — '" + sceneName + "' waiting its turn (harvests are serialized).")); while (DonorPhotonGuard.WindowActive) { yield return null; } DonorRuntime.Log.LogMessage((object)$"[HARVEST] '{sceneName}' proceeding after {Time.realtimeSinceStartup - t0:F2}s wait."); } int guardToken = DonorPhotonGuard.Begin(sceneName); GlobalAudioManager realAudio = Global.AudioManager; GlobalCombatManager realCombat = Global.CombatManager; EnvironmentConditions realEnv = EnvironmentConditions.Instance; AISquadManager realSquads = AISquadManager.Instance; TerrainMeshQualityManager realTerrainMesh = TerrainMeshQualityManager.Instance; Material realSkybox = RenderSettings.skybox; SkySnapshot.Log("pre-harvest '" + sceneName + "'", DonorRuntime.Log); float[] realWaterGlobals = DonorDisplayRepair.CaptureWaterGlobals(); int frozenAtLoad = 0; UnityAction<Scene, LoadSceneMode> freezeOnLoad = delegate(Scene s, LoadSceneMode m) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 if ((int)m == 1 && !(((Scene)(ref s)).name != sceneName)) { GameObject[] rootGameObjects4 = ((Scene)(ref s)).GetRootGameObjects(); foreach (GameObject val6 in rootGameObjects4) { DonorDisplayRepair.NeutralizeCamerasUnder(val6, "sceneLoaded"); val6.SetActive(false); frozenAtLoad++; } } }; SceneManager.sceneLoaded += freezeOnLoad; DonorDisplayRepair.ArmCameraSentinel(sceneName); bool restoresRan = false; object result = null; AssetBundle meshBundleShield = null; try { AsyncOperation load; try { load = SceneManager.LoadSceneAsync(sceneName, (LoadSceneMode)1); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[HARVEST] LoadSceneAsync threw: " + ex.Message)); onResult(null); yield break; } if (load == null) { DonorRuntime.Log.LogWarning((object)("[HARVEST] LoadSceneAsync returned null — bad scene name '" + sceneName + "'?")); onResult(null); yield break; } float hangWarnAt = Time.realtimeSinceStartup + 30f; float abandonAt = Time.realtimeSinceStartup + 30f + 120f; while (!load.isDone) { DonorPhotonGuard.Refresh(guardToken); if (hangWarnAt > 0f && Time.realtimeSinceStartup >= hangWarnAt) { DonorRuntime.Log.LogWarning((object)$"[HARVEST] donor scene '{sceneName}' load still not done after {30f:F0}s — possible hang (check output_log.txt for Unity-side load errors)."); hangWarnAt = -1f; } if (Time.realtimeSinceStartup >= abandonAt) { DonorRuntime.Log.LogWarning((object)$"[HARVEST] ABANDONING donor scene '{sceneName}' — load STILL not done after {150f:F0}s (truly wedged). Closing the guard window + restoring singletons so saves/harvests aren't starved for the rest of the session; the scene is left loading."); onResult(null); yield break; } yield return null; } DonorPhotonGuard.Refresh(guardToken); RunSingletonRestores(); DonorDisplayRepair.RestoreSkybox(realSkybox, "load-done"); Scene sceneByName = SceneManager.GetSceneByName(sceneName); if (!((Scene)(ref sceneByName)).IsValid()) { DonorRuntime.Log.LogWarning((object)("[HARVEST] donor scene '" + sceneName + "' not valid after load.")); onResult(null); yield break; } DonorRuntime.Log.LogMessage((object)$"[HARVEST] donor scene loaded in {Time.realtimeSinceStartup - t0:F2}s ({((Scene)(ref sceneByName)).rootCount} roots)."); int num = 0; GameObject[] rootGameObjects = ((Scene)(ref sceneByName)).GetRootGameObjects(); foreach (GameObject val in rootGameObjects) { num += val.GetComponentsInChildren<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 ex2) { DonorRuntime.Log.LogWarning((object)("[HARVEST] processing threw: " + ex2.Message + "\n" + ex2.StackTrace)); } float u0 = Time.realtimeSinceStartup; DonorPhotonGuard.Refresh(guardToken); meshBundleShield = TerrainGuard.BeginMeshBundleShield(sceneByName, "pre-unload"); AsyncOperation val5 = SceneManager.UnloadSceneAsync(sceneByName); if (val5 != null) { yield return val5; } TerrainGuard.EndMeshBundleShield(meshBundleShield, "post-unload"); meshBundleShield = null; CyclesThisSession++; DonorRuntime.Log.LogMessage((object)$"[HARVEST] donor scene unloaded in {Time.realtimeSinceStartup - u0:F2}s (session donor cycles={CyclesThisSession})."); float realtimeSinceStartup = Time.realtimeSinceStartup; try { LightProbes.Tetrahedralize(); DonorRuntime.Log.LogMessage((object)$"[PROBE-FIX] light probes re-tetrahedralized in {Time.realtimeSinceStartup - realtimeSinceStartup:F2}s (LightProbesManager crash mitigation)."); } catch (Exception ex3) { DonorRuntime.Log.LogWarning((object)("[PROBE-FIX] Tetrahedralize failed: " + ex3.Message)); } DonorDisplayRepair.RestoreSkybox(realSkybox, "post-unload"); DonorDisplayRepair.RepairDisplayChain("post-unload"); DonorDisplayRepair.CheckWaterGlobals(realWaterGlobals, "post-unload", restore: true); SkySnapshot.Log("post-unload", DonorRuntime.Log); if ((Object)(object)DonorRuntime.Runner != (Object)null) { DonorRuntime.Runner.StartCoroutine(DonorDisplayRepair.DeferredSkyCheck(realSkybox, realWaterGlobals)); } } finally { SceneManager.sceneLoaded -= freezeOnLoad; DonorDisplayRepair.DisarmCameraSentinel(); TerrainGuard.EndMeshBundleShield(meshBundleShield, "finally"); RunSingletonRestores(); DonorPhotonGuard.End(guardToken); } if (frozenAtLoad > 0) { DonorRuntime.Log.LogMessage((object)$"[HARVEST] froze {frozenAtLoad} donor root(s) at sceneLoaded — donor Start/Update never ran (global-state protection)."); } else { DonorRuntime.Log.LogWarning((object)"[HARVEST] sceneLoaded freeze did NOT fire — donor scripts may have run Start/Update (watch for [DISPLAY-FIX]/global-state symptoms)."); } DonorAudioRegistry.PruneDeadSoundPlayers(); SoulSpotGuard.PruneDead(); DonorAudioRegistry.AuditAudioRegistries("post-harvest"); DonorRuntime.Log.LogMessage((object)string.Format("[HARVEST] done in {0:F2}s total — result={1}.", Time.realtimeSinceStartup - t0, (result != null) ? "OK" : "FAILED")); onResult(result); void RunSingletonRestores() { if (!restoresRan) { restoresRan = true; DonorSingletonRestore.RestoreAudioManager(realAudio); DonorSingletonRestore.RestoreCombatManager(realCombat); DonorSingletonRestore.RestoreEnvironment(realEnv); DonorSingletonRestore.RestoreSquadManager(realSquads); DonorSingletonRestore.RestoreTerrainMeshManager(realTerrainMesh); } } } } [HarmonyPatch(typeof(CharacterAI), "NetworkInit")] public static class DonorNetworkInitGuard { [HarmonyPrefix] private static bool Prefix(CharacterAI __instance) { return OnNetworkInit(__instance); } internal static bool OnNetworkInit(CharacterAI ai) { try { if (!PhotonNetwork.isNonMasterClientInRoom) { return true; } if ((Object)(object)ai == (Object)null || !DonorPhotonGuard.IsDonorSceneObject(((Component)ai).gameObject)) { return true; } string activeDonorScene = DonorPhotonGuard.ActiveDonorScene; DonorRuntime.Log.LogMessage((object)("[HARVEST-GUARD] suppressed guest NetworkInit replica-conversion: '" + ((Object)ai).name + "' in donor scene '" + activeDonorScene + "' — donor keeps CharacterAI, the template stays master-shaped.")); return false; } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[HARVEST-GUARD] prefix error (vanilla NetworkInit allowed): " + ex.Message)); return true; } } } [HarmonyPatch(typeof(NetworkingPeer), "RegisterPhotonView")] public static class RegisterPhotonViewGuard { [HarmonyPrefix] private static bool Prefix(NetworkingPeer __instance, PhotonView netView) { return DonorPhotonGuard.OnRegister(__instance, netView); } } public static class DonorPhotonGuard { private const float ExpirySeconds = 30f; private static readonly GuardWindow _window = new GuardWindow(30.0); private static string _scene; private static int _skipped; private static bool Active => _window.Active((double)Time.realtimeSinceStartup); public static bool WindowActive => Active; public static string ActiveDonorScene { get { if (!Active) { return null; } return _scene; } } public static bool IsDonorSceneObject(GameObject go) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)go == (Object)null) { return false; } string activeDonorScene = ActiveDonorScene; if (activeDonorScene != null) { Scene scene = go.scene; return ((Scene)(ref scene)).name == activeDonorScene; } return false; } public static int Begin(string donorScene) { float realtimeSinceStartup = Time.realtimeSinceStartup; if (_window.Owner != 0) { DonorRuntime.Log.LogWarning((object)("[PHOTON-GUARD] window for '" + _scene + "' still open at Begin — " + (_window.Expired((double)realtimeSinceStartup) ? "EXPIRED (wedged harvest; taking over — its late End will be ignored)." : "NOT expired: harvest serialization broke, overlapping windows!"))); } int num = _window.Begin((double)realtimeSinceStartup); _scene = donorScene; _skipped = 0; DonorRuntime.Log.LogMessage((object)$"[PHOTON-GUARD] window OPEN for donor scene '{donorScene}' (token {num})."); return num; } public static void Refresh(int token) { _window.Refresh(token, (double)Time.realtimeSinceStartup); } public static void End(int token) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Invalid comparison between Unknown and I4 //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 GuardEndResult val = _window.End(token); if ((int)val != 1) { if ((int)val == 2) { DonorRuntime.Log.LogWarning((object)$"[PHOTON-GUARD] stale End (token {token}) ignored — the window is now owned by token {_window.Owner} for '{_scene}'."); return; } string text = (((double)Time.realtimeSinceStartup >= _window.ExpireAt) ? " (deadline LAPSED before close — the tail of this harvest ran unguarded; heartbeat gap?)" : ""); int num = ((PhotonNetwork.networkingPeer != null && PhotonNetwork.networkingPeer.photonViewList != null) ? PhotonNetwork.networkingPeer.photonViewList.Count : (-1)); DonorRuntime.Log.LogMessage((object)$"[PHOTON-GUARD] window CLOSED for '{_scene}'{text}: suppressed {_skipped} donor view registration(s); registry holds {num} view(s)."); _scene = null; _skipped = 0; } } public static bool OnRegister(NetworkingPeer peer, PhotonView netView) { //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)netView == (Object)null) { return true; } if (IsDonorSceneObject(((Component)netView).gameObject)) { netView.removedFromLocalViewList = true; _skipped++; DonorRuntime.Log.LogMessage((object)$"[PHOTON-GUARD] suppressed donor view: id={netView.viewID} go='{((Object)((Component)netView).gameObject).name}'."); return false; } if (peer != null && peer.photonViewList != null && peer.photonViewList.TryGetValue(netView.viewID, out var value) && (Object)(object)value != (Object)(object)netView) { object obj; Scene scene; if (!((Object)(object)value != (Object)null)) { obj = "<destroyed>"; } else { string[] obj2 = new string[5] { "'", ((Object)((Component)value).gameObject).name, "' (scene '", null, null }; scene = ((Component)value).gameObject.scene; obj2[3] = ((Scene)(ref scene)).name; obj2[4] = "')"; obj = string.Concat(obj2); } string text = (string)obj; ManualLogSource log = DonorRuntime.Log; object[] obj3 = new object[4] { netView.viewID, ((Object)((Component)netView).gameObject).name, null, null }; scene = ((Component)netView).gameObject.scene; obj3[2] = ((Scene)(ref scene)).name; obj3[3] = text; log.LogMessage((object)string.Format("[PHOTON-RECON] duplicate view id {0}: new='{1}' (scene '{2}') will destroy old={3}.", obj3)); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[PHOTON-GUARD] prefix error (registration allowed): " + ex.Message)); } return true; } public static void DumpRegistry() { //IL_0227: Unknown result type (might be due to invalid IL or missing references) //IL_022c: Unknown result type (might be due to invalid IL or missing references) NetworkingPeer peer = PhotonNetwork.networkingPeer; if (peer == null || peer.photonViewList == null) { DonorRuntime.Log.LogMessage((object)"[PHOTON-RECON] no networking peer / view list."); return; } int num = peer.photonViewList.Values.Count((PhotonView v) => (Object)(object)v == (Object)null); DonorRuntime.Log.LogMessage((object)$"[PHOTON-RECON] registry: {peer.photonViewList.Count} view(s), {num} destroyed-but-still-listed."); try { List<int> manuallyAllocatedViewIds = PhotonNetwork.manuallyAllocatedViewIds; if (manuallyAllocatedViewIds == null) { DonorRuntime.Log.LogMessage((object)"[PHOTON-RECON] manuallyAllocatedViewIds: <null> (Photon not initialized)."); } else { List<int> list = manuallyAllocatedViewIds.OrderBy((int i) => i).ToList(); int num2 = list.Count((int i) => !peer.photonViewList.ContainsKey(i)); string arg = ((list.Count == 0) ? "" : (": " + string.Join(", ", list.Take(60)) + ((list.Count > 60) ? $" … (+{list.Count - 60} more)" : ""))); DonorRuntime.Log.LogMessage((object)($"[PHOTON-RECON] manuallyAllocatedViewIds: {list.Count} id(s), " + $"{num2} with no registered view (leak candidates / deferred corpse releases){arg}")); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[PHOTON-RECON] manuallyAllocatedViewIds read failed: " + ex.Message)); } foreach (KeyValuePair<int, PhotonView> item in peer.photonViewList.OrderBy((KeyValuePair<int, PhotonView> k) => k.Key).Take(40)) { object obj; if (!((Object)(object)item.Value != (Object)null)) { obj = "<destroyed>"; } else { string name = ((Object)((Component)item.Value).gameObject).name; Scene scene = ((Component)item.Value).gameObject.scene; obj = $"go='{name}' scene='{((Scene)(ref scene)).name}' active={((Component)item.Value).gameObject.activeInHierarchy}"; } string arg2 = (string)obj; DonorRuntime.Log.LogMessage((object)$"[PHOTON-RECON] id={item.Key} {arg2}"); } if (peer.photonViewList.Count > 40) { DonorRuntime.Log.LogMessage((object)$"[PHOTON-RECON] … {peer.photonViewList.Count - 40} more (photondump shows the low/baked id range)."); } } } public static class DonorRuntime { public static ManualLogSource Log => Plugin.Log; public static MonoBehaviour Runner => (MonoBehaviour)(object)Plugin.Instance; public static bool IsSanePosition(Vector3 p) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) return WorldPosition.IsSane(p.x, p.y, p.z); } } [HarmonyPatch(typeof(SaveManager), "Save", new Type[] { typeof(bool), typeof(bool) })] public static class DonorSaveGuard { [HarmonyPrefix] private static bool Prefix(SaveManager __instance, bool _async, bool _forceSaveEnvironment) { try { if (!DonorPhotonGuard.WindowActive) { return true; } __instance.SetSaveRequired(); DonorRuntime.Log.LogWarning((object)$"[SAVE-GUARD] save requested mid-harvest (async={_async}, forceEnv={_forceSaveEnvironment}) — deferred via SetSaveRequired; vanilla re-fires it after the window closes."); return false; } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[SAVE-GUARD] prefix error (save allowed): " + ex.Message)); return true; } } } internal static class DonorSceneTable { private static Dictionary<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(DonorRuntime.Log); } } internal static class DonorSingletonRestore { internal static void RestoreAudioManager(GlobalAudioManager original) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) GlobalAudioManager audioManager = Global.AudioManager; if ((Object)(object)original == (Object)null) { if ((Object)(object)audioManager != (Object)null) { DonorRuntime.Log.LogWarning((object)"[AMBIENCE-FIX] no audio manager existed pre-load but one does now — leaving it in place."); } } else if (!((Object)(object)audioManager == (Object)(object)original)) { Global.AudioManager = original; ManualLogSource log = DonorRuntime.Log; object obj; if (!((Object)(object)audioManager == (Object)null)) { Scene scene = ((Component)audioManager).gameObject.scene; obj = "'" + ((Scene)(ref scene)).name + "'s"; } else { obj = "already destroyed"; } log.LogMessage((object)("[AMBIENCE-FIX] donor GlobalAudioManager hijacked the singleton (found " + (string?)obj + ") — restored the real one (bug-2 fix).")); } } internal static void RestoreCombatManager(GlobalCombatManager original) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) GlobalCombatManager combatManager = Global.CombatManager; if ((Object)(object)original == (Object)null) { if ((Object)(object)combatManager != (Object)null) { DonorRuntime.Log.LogWarning((object)"[COMBAT-FIX] no combat manager existed pre-load but one does now — leaving it in place."); } } else if (!((Object)(object)combatManager == (Object)(object)original)) { Global.CombatManager = original; ManualLogSource log = DonorRuntime.Log; object obj; if (!((Object)(object)combatManager == (Object)null)) { Scene scene = ((Component)combatManager).gameObject.scene; obj = "'" + ((Scene)(ref scene)).name + "'s"; } else { obj = "already destroyed"; } log.LogMessage((object)("[COMBAT-FIX] donor GlobalCombatManager hijacked the singleton (found " + (string?)obj + ") — restored the real one (bug-12 fix).")); } } internal static void RestoreEnvironment(EnvironmentConditions original) { //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) EnvironmentConditions instance = EnvironmentConditions.Instance; Scene scene; if ((Object)(object)original == (Object)null) { if ((Object)(object)instance != (Object)null) { DonorRuntime.Log.LogWarning((object)"[ENV-FIX] no EnvironmentConditions existed pre-load but one does now — leaving it in place."); return; } EnvironmentConditions val = Object.FindObjectOfType<EnvironmentConditions>(); if (!((Object)(object)val == (Object)null)) { EnvironmentConditions.Instance = val; ManualLogSource log = DonorRuntime.Log; scene = ((Component)val).gameObject.scene; log.LogWarning((object)("[ENV-FIX] EnvironmentConditions.Instance was ALREADY dangling before this harvest — adopted the live '" + ((Scene)(ref scene)).name + "' instance (compounded bug-12).")); } } else if (!((Object)(object)instance == (Object)(object)original)) { EnvironmentConditions.Instance = original; ManualLogSource log2 = DonorRuntime.Log; object obj; if (!((Object)(object)instance == (Object)null)) { scene = ((Component)instance).gameObject.scene; obj = "'" + ((Scene)(ref scene)).name + "'s"; } else { obj = "already destroyed"; } log2.LogMessage((object)("[ENV-FIX] donor EnvironmentConditions hijacked the singleton (found " + (string?)obj + ") — restored the real one (bug-12 true root cause).")); } } internal static void RestoreSquadManager(AISquadManager original) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) AISquadManager instance = AISquadManager.Instance; if ((Object)(object)original == (Object)null) { if ((Object)(object)instance != (Object)null) { DonorRuntime.Log.LogWarning((object)"[SQUAD-FIX] no AISquadManager existed pre-load but one does now — leaving it in place."); } } else if (!((Object)(object)instance == (Object)(object)original)) { AISquadManager.Instance = original; ManualLogSource log = DonorRuntime.Log; object obj; if (!((Object)(object)instance == (Object)null)) { Scene scene = ((Component)instance).gameObject.scene; obj = "'" + ((Scene)(ref scene)).name + "'s"; } else { obj = "already destroyed"; } log.LogMessage((object)("[SQUAD-FIX] donor AISquadManager hijacked the singleton (found " + (string?)obj + ") — restored the real one (bug-16 fix).")); } } internal static void RestoreTerrainMeshManager(TerrainMeshQualityManager original) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) TerrainMeshQualityManager instance = TerrainMeshQualityManager.Instance; if ((Object)(object)original == (Object)null) { if ((Object)(object)instance != (Object)null) { DonorRuntime.Log.LogWarning((object)"[TMESH-FIX] no live TerrainMeshQualityManager to restore (none existed pre-load, or the captured one has since been destroyed) but one exists now — leaving it in place."); } } else if (!((Object)(object)instance == (Object)(object)original)) { TerrainMeshQualityManager.Instance = original; ManualLogSource log = DonorRuntime.Log; object obj; if (!((Object)(object)instance == (Object)null)) { Scene scene = ((Component)instance).gameObject.scene; obj = "'" + ((Scene)(ref scene)).name + "'s"; } else { obj = "already destroyed"; } log.LogMessage((object)("[TMESH-FIX] donor TerrainMeshQualityManager hijacked the singleton (found " + (string?)obj + ") — restored the real one (V32-b fix, part 1).")); } } } public static class DonorVerbs { public static void RegisterAll(CommandRegistry c) { c.Register("photondump", "Photon view-registry health (Bug 1).", (Action<string[]>)delegate { DonorPhotonGuard.DumpRegistry(); }); c.Register("audiodump", "GlobalAudioManager registrant audit (Bug 2).", (Action<string[]>)delegate { DonorHarvest.AuditAudioRegistries("manual audiodump"); }); c.Register("audioprune", "Drop destroyed SoundPlayers from the static registry + audit.", (Action<string[]>)delegate { DonorHarvest.PruneDeadSoundPlayers(); DonorHarvest.AuditAudioRegistries("post-prune"); }); c.Register("terraindump", "Snapshot terrain health now ([TERRAIN] meshTiles/brokenMesh/lod2Missing; render-hole diagnosis).", (Action<string[]>)delegate { TerrainGuard.Dump("manual terraindump"); }); c.Register("terrainfix", "Force blanked terrain tiles to the COARSE Lod2 mesh. WARNING: draws the ground at the wrong height (Bug 32) — a zone reload is the clean fix.", (Action<string[]>)delegate { TerrainGuard.RepairNow("manual terrainfix"); }); } } public static class ExpeditionHarvest { private sealed class PlayerMark { public Character Ch; public string Uid; public Vector3 Pos; public Quaternion Rot; } private const float WatchdogSeconds = 300f; private static string _donorScene; private static string _homeScene; private static readonly List<PlayerMark> _marks = new List<PlayerMark>(); private static Func<Scene, object> _payload; private static Action<object> _onDone; private static object _result; private static int _leg; private static bool _payloadRan; private static int _gameplayDoneLeg; private static NetworkLevelLoader _loader; private static int _generation; private static bool _returnRequested; private static bool _restoring; private static int _returnRetries; private static bool _rescued; private static int _resumeCount; private static int _lastResumeFrame; private static string _endScene; private static string _endReason; public static float ReturnLegDelaySeconds = 2f; private const float OutboundGraceSeconds = 30f; internal const string RoomPropKey = "CK_EXPED"; private static bool _roomPropStamped; private static int _sceneDoneCount; private static int _gameplayDoneCount; private const int MaxReturnRetries = 2; private const float RescueGraceSeconds = 60f; public static bool InProgress { get; private set; } public static bool LastTripEndedHome { get; private set; } = true; public static float ReturnRetrySeconds { get { if (DkConfig.Expedition.ReturnRetrySeconds == null) { return 20f; } return Mathf.Max(0f, DkConfig.Expedition.ReturnRetrySeconds.Value); } } public static bool Begin(string donorScene, Func<Scene, object> useScene, Action<object> onDone) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_031e: Unknown result type (might be due to invalid IL or missing references) //IL_0328: Expected O, but got Unknown //IL_0328: Unknown result type (might be due to invalid IL or missing references) //IL_0332: Expected O, but got Unknown //IL_0344: Unknown result type (might be due to invalid IL or missing references) //IL_034e: Expected O, but got Unknown //IL_034e: Unknown result type (might be due to invalid IL or missing references) //IL_0358: Expected O, but got Unknown //IL_036a: Unknown result type (might be due to invalid IL or missing references) //IL_0374: Expected O, but got Unknown //IL_0374: Unknown result type (might be due to invalid IL or missing references) //IL_037e: Expected O, but got Unknown //IL_0227: Unknown result type (might be due to invalid IL or missing references) //IL_023e: Unknown result type (might be due to invalid IL or missing references) //IL_0243: Unknown result type (might be due to invalid IL or missing references) //IL_0250: Unknown result type (might be due to invalid IL or missing references) //IL_0255: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrEmpty(donorScene) || useScene == null) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] Begin: bad arguments."); return false; } if (InProgress) { DonorRuntime.Log.LogWarning((object)$"[EXPEDITION] already running ('{_donorScene}', leg {_leg}) — refusing a second one."); return false; } Scene activeScene = SceneManager.GetActiveScene(); if (string.Equals(((Scene)(ref activeScene)).name, donorScene, StringComparison.OrdinalIgnoreCase)) { DonorRuntime.Log.LogMessage((object)("[EXPEDITION] '" + donorScene + "' is the ACTIVE scene — running the payload in place (no loading screens).")); LastTripEndedHome = true; object obj = RunPayload(activeScene, useScene); try { onDone?.Invoke(obj); } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] onDone threw: " + ex.Message)); } return true; } if (StorySense.IsPrologueActive()) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] refusing to depart the prologue/tutorial — a round trip here consumes one-shot story beats (finish the intro first)."); return false; } NetworkLevelLoader instance = NetworkLevelLoader.Instance; if ((Object)(object)instance == (Object)null) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] no NetworkLevelLoader yet."); return false; } if (instance.InLoading) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] a load is already in progress — try again once it settles."); return false; } if (DonorPhotonGuard.WindowActive) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] a donor-scene harvest window is open (an additive donor load is in flight — the vanilla loader does not see it) — refusing to depart under it. Try again in a few seconds."); return false; } if (PhotonNetwork.isNonMasterClientInRoom) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] only the HOST can run an expedition (it drives two area switches for the whole party) — ask the host."); return false; } int num = 0; try { PhotonPlayer[] otherPlayers = PhotonNetwork.otherPlayers; num = ((otherPlayers != null) ? otherPlayers.Length : 0); } catch (Exception ex2) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] otherPlayers read threw (" + ex2.GetType().Name + ") — co-op gate is judging as solo.")); } if (ExpeditionCoop.ShouldRefuseBegin(DkConfig.Expedition.AllowCoop.Value, PhotonNetwork.inRoom, num)) { DonorRuntime.Log.LogWarning((object)($"[EXPEDITION] refusing to start with {num} other player(s) connected — " + "a mid-trip guest gate wedge can strand the WHOLE party in the donor region and the watchdog cannot recover it. Set [Expedition] AllowCoop = true to allow co-op expeditions (testing only; every guest should run [Expedition] GuestAutoContinue = true).")); return false; } if ((Object)(object)DonorRuntime.Runner == (Object)null) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] no DonorKit plugin host for coroutines."); return false; } _marks.Clear(); CharacterManager instance2 = CharacterManager.Instance; if ((Object)(object)instance2 != (Object)null) { for (int i = 0; i < instance2.PlayerCharacters.Count; i++) { Character character = instance2.GetCharacter(instance2.PlayerCharacters.Values[i]); if (!((Object)(object)character == (Object)null)) { _marks.Add(new PlayerMark { Ch = character, Uid = UID.op_Implicit(character.UID), Pos = ((Component)character).transform.position, Rot = ((Component)character).transform.rotation }); } } } if (_marks.Count == 0) { DonorRuntime.Log.LogWarning((object)"[EXPEDITION] no player characters to mark — not starting."); return false; } _donorScene = donorScene; _homeScene = ((Scene)(ref activeScene)).name; _payload = useScene; _onDone = onDone; _result = null; _payloadRan = false; _gameplayDoneLeg = 0; _leg = 1; _returnRequested = false; _restoring = false; _returnRetries = 0; _rescued = false; _resumeCount = 0; _lastResumeFrame = 0; LastTripEndedHome = false; _loader = instance; _generation++; InProgress = true; try { NetworkLevelLoader loader = _loader; loader.onSceneLoadingDone = (UnityAction)Delegate.Combine((Delegate?)(object)loader.onSceneLoadingDone, (Delegate?)new UnityAction(HandleSceneLoadingDone)); NetworkLevelLoader loader2 = _loader; loader2.onGameplayLoadingDone = (UnityAction)Delegate.Combine((Delegate?)(object)loader2.onGameplayLoadingDone, (Delegate?)new UnityAction(HandleGameplayLoadingDone)); NetworkLevelLoader loader3 = _loader; loader3.OnResumeAfterLoading = (UnityAction)Delegate.Combine((Delegate?)(object)loader3.OnResumeAfterLoading, (Delegate?)new UnityAction(HandleResumeAfterLoading)); StampRoomProperty(); DonorRuntime.Runner.StartCoroutine(AutoContinue(_generation)); DonorRuntime.Runner.StartCoroutine(Watchdog(_generation)); DonorRuntime.Log.LogMessage((object)("[EXPEDITION] departing '" + _homeScene + "' → '" + _donorScene + "' " + $"({_marks.Count} player position(s) marked; the game saves on each leg, vanilla door behavior).")); _loader.RequestSwitchArea(_donorScene, 0, 1.5f, false); DonorRuntime.Runner.StartCoroutine(OutboundWatch(_generation)); return true; } catch (Exception arg) { DonorRuntime.Log.LogError((object)($"[EXPEDITION] failed to launch '{_donorScene}': {arg}. " + "Releasing the expedition guard — nobody left home, so nothing is stranded.")); _onDone = null; Finish("launch threw"); return false; } } private static IEnumerator OutboundWatch(int gen) { float t0 = Time.unscaledTime; while (Time.unscaledTime - t0 < 30f) { if (!InProgress || gen != _generation) { yield break; } yield return (object)new WaitForSeconds(0.5f); } if (InProgress && gen == _generation && _leg == 1 && _gameplayDoneLeg == 0 && !LoaderBusy() && IsActiveScene(_homeScene)) { DonorRuntime.Log.LogWarning((object)($"[EXPEDITION] the switch to '{_donorScene}' was requested {30f:F0}s ago " + "but no load ever started and we are still in '" + _homeScene + "' — the donor scene never loaded. Aborting (nothing was moved).")); Finish("the outbound to '" + _donorScene + "' never started"); } } public static string Status() { return (InProgress ? ($"expedition IN PROGRESS: '{_homeScene}' → '{_donorScene}' leg {_leg}/2, payload ran={_payloadRan}, " + $"gate phase reached for leg {_gameplayDoneLeg}, returnRequested={_returnRequested}, " + $"restoring={_restoring}, resumes={_resumeCount}, returnRetries={_returnRetries}") : $"no expedition in progress (last trip ended home: {LastTripEndedHome})") + $" (return-leg delay {ReturnLegDelaySeconds:F1}s, return retry {ReturnRetrySeconds:F0}s)"; } public static string ForceReset() { if (!InProgress) { return "[EXPEDITION] no expedition in progress — nothing to reset."; } string text = $"'{_homeScene}' → '{_donorScene}' leg {_leg}/2"; _result = null; Finish("force reset (expeditionreset verb)"); return "[EXPEDITION] FORCE RESET a stuck expedition (" + text + "). The guard is open again and any waiting caller (SpawnKit's menu/verb chain) was handed an aborted-trip result — nothing chains onto a force reset. If the party is not home, recover with 'goto <scene>'. Please report the log — Begin is supposed to release the guard on its own."; } private static void StampRoomProperty() { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown _roomPropStamped = false; try { if (PhotonNetwork.inRoom && PhotonNetwork.room != null) { Hashtable val = new Hashtable(); val[(object)"CK_EXPED"] = _generation; PhotonNetwork.room.SetCustomProperties(val, (Hashtable)null, false); _roomPropStamped = true; DonorRuntime.Log.LogMessage((object)(string.Format("[EXPEDITION] stamped room property {0}={1} — ", "CK_EXPED", _generation) + "guests with [Expedition] GuestAutoContinue=true will auto-pass their continue gates.")); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] stamping the CK_EXPED room property threw (" + ex.GetType().Name + ": " + ex.Message + ") — guests must pass their gates by hand.")); } } private static void ClearRoomProperty() { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown if (!_roomPropStamped) { return; } _roomPropStamped = false; try { if (PhotonNetwork.inRoom && PhotonNetwork.room != null) { Hashtable val = new Hashtable(); val[(object)"CK_EXPED"] = null; PhotonNetwork.room.SetCustomProperties(val, (Hashtable)null, false); DonorRuntime.Log.LogMessage((object)"[EXPEDITION] cleared room property CK_EXPED."); } } catch (Exception ex) { DonorRuntime.Log.LogWarning((object)("[EXPEDITION] clearing the CK_EXPED room property threw (" + ex.GetType().Name + ": " + ex.Message + ").")); } } private static bool LoaderBusy() { NetworkLevelLoader instance = NetworkLevelLoader.Instance; if ((Object)(object)instance == (Object)null) { return false; } try { return instance.InLoading || instance.m_prepingLoadLevel; } catch { return false; } } private static void HandleSceneLoadingDone() { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) if (InProgress) { ManualLogSource log = DonorRuntime.Log; string text = $"[EXPEDITION] phase: scene-loading-done #{++_sceneDoneCount} "; object arg = _leg; Scene activeScene = SceneManager.GetActiveScene(); log.LogMessage((object)(text + $"(leg {arg}, scene '{((Scene)(ref activeScene)).name}', frame {Time.frameCount}).")); } } private static void HandleGameplayLoadingDone() { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) if (!InProgress) { return; } Scene activeScene = SceneManager.GetActiveScene(); string b = ((_leg == 1) ? _donorScene : _homeScene); DonorRuntime.Log.LogMessage((object)($"[EXPEDITION] phase: gameplay-loading-done #{++_gameplayDoneCount} " + $"(leg {_leg}, scene '{((Scene)(ref activeScene)).name}', frame {Time.frameCount}).")); if (string.Equals(((Scene)(ref activeScene)).name, b, StringComparison.OrdinalIgnoreCase)) { _gameplayDoneLeg = _leg; if (_leg == 1 && !_payloadRan) { _payloadRan = true; _result = RunPayload(activeScene, _payload); } } } private static void HandleResume