Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of DungeonLoadingOptimizer v0.1.0
DungeonLoadingOptimizer.dll
Decompiled a day agousing System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using DunGen; using DunGen.Graph; using DungeonLoadingOptimizer.Compat; using DungeonLoadingOptimizer.Config; using DungeonLoadingOptimizer.Diagnostics; using DungeonLoadingOptimizer.Patches; using DungeonLoadingOptimizer.Reflection; using DungeonLoadingOptimizer.Runtime; using HarmonyLib; using Microsoft.CodeAnalysis; using Unity.AI.Navigation; using Unity.Netcode; using UnityEngine; using UnityEngine.AI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("RustyAstroboy")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("ALPHA. Cuts Lethal Company interior loading time and removes the navmesh lag spike when landing. Multiplayer determinism is not yet verified - do not use on a serious save.")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0+846cf29d923c021749459e6e86b39a11b09f74c8")] [assembly: AssemblyProduct("DungeonLoadingOptimizer")] [assembly: AssemblyTitle("DungeonLoadingOptimizer")] [assembly: AssemblyVersion("0.1.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace DungeonLoadingOptimizer { internal static class Log { private static ManualLogSource _source; public static bool IsVerbose { get { if (PluginConfig.LogLevel != null) { return PluginConfig.LogLevel.Value == LogVerbosity.Verbose; } return false; } } private static bool AtLeastSummary { get { if (PluginConfig.LogLevel != null) { return PluginConfig.LogLevel.Value != LogVerbosity.Off; } return true; } } public static void Init(ManualLogSource source) { _source = source; } public static void Info(string message) { if (AtLeastSummary) { ManualLogSource source = _source; if (source != null) { source.LogInfo((object)message); } } } public static void Debug(string message) { if (IsVerbose) { ManualLogSource source = _source; if (source != null) { source.LogInfo((object)message); } } } public static void Warn(string message) { ManualLogSource source = _source; if (source != null) { source.LogWarning((object)message); } } public static void Error(string message) { ManualLogSource source = _source; if (source != null) { source.LogError((object)message); } } } [BepInPlugin("RustyAstroboy.DungeonLoadingOptimizer", "DungeonLoadingOptimizer", "0.1.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { private Harmony _harmony; private void Awake() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown Log.Init(((BaseUnityPlugin)this).Logger); PluginConfig.BindAll(((BaseUnityPlugin)this).Config); ModCompat.Detect(); GameRefs.Resolve(); _harmony = new Harmony("RustyAstroboy.DungeonLoadingOptimizer"); TryPatch(typeof(LoadLifecyclePatch), enabled: true, GameRefs.LifecycleAvailable); TryPatch(typeof(FrameBudgetPatch), enabled: true, GameRefs.FrameBudgetAvailable); TryPatch(typeof(FinishGeneratingLevelPatch), enabled: true, GameRefs.FinishGeneratingLevel != null); TryPatch(typeof(NavMeshBuildInterceptPatch), enabled: true, GameRefs.NavMeshAvailable); TryPatch(typeof(TagPairCachePatch), PluginConfig.TagPairCacheEnabled.Value, GameRefs.TagPairCacheAvailable); if (PostRpcProfilerPatch.ShouldInstrument) { PostRpcProfilerPatch.Apply(_harmony); } if (!PluginConfig.Enabled.Value) { Log.Warn("General.Enabled = false: every module is off. Only the diagnostics are running (control case)."); } ReportPatchedMethods(); Log.Info("DungeonLoadingOptimizer 0.1.0 loaded (validated against game v81)."); } private void ReportPatchedMethods() { List<string> list = new List<string>(); foreach (MethodBase patchedMethod in _harmony.GetPatchedMethods()) { Patches patchInfo = Harmony.GetPatchInfo(patchedMethod); if (patchInfo != null && (Owns(patchInfo.Prefixes) || Owns(patchInfo.Postfixes) || Owns(patchInfo.Transpilers) || Owns(patchInfo.Finalizers))) { list.Add(patchedMethod.DeclaringType?.Name + "." + patchedMethod.Name); } } if (list.Count == 0) { Log.Error("Harmony reports ZERO methods hooked by this mod. Nothing will happen at runtime."); return; } list.Sort(); Log.Info("Hooked " + list.Count + " method(s): " + string.Join(", ", list.ToArray())); } private static bool Owns(IEnumerable<Patch> patches) { foreach (Patch patch in patches) { if (patch.owner == "RustyAstroboy.DungeonLoadingOptimizer") { return true; } } return false; } private void TryPatch(Type patchClass, bool enabled, bool available) { if (!available) { Log.Warn(patchClass.Name + " skipped: a game member it needs was not found."); return; } if (!enabled) { Log.Debug(patchClass.Name + " skipped: module disabled in the config."); return; } try { _harmony.CreateClassProcessor(patchClass, true).Patch(); Log.Debug(patchClass.Name + " applied."); } catch (Exception ex) { Log.Error(patchClass.Name + " failed to apply. The other modules keep working. Exception:\n" + ex); } } } internal static class PluginInfo { public const string Guid = "RustyAstroboy.DungeonLoadingOptimizer"; public const string Name = "DungeonLoadingOptimizer"; public const string Author = "RustyAstroboy"; public const string Version = "0.1.0"; public const string ValidatedGameVersion = "81"; } } namespace DungeonLoadingOptimizer.Runtime { internal static class DeferredBakeScope { private sealed class Scope : IDisposable { private bool _disposed; public Scope() { _depth++; } public void Dispose() { if (!_disposed) { _disposed = true; if (_depth > 0) { _depth--; } } } } private static readonly List<NavMeshSurface> Captured = new List<NavMeshSurface>(8); private static int _depth; public static bool Capturing => _depth > 0; public static IDisposable Begin() { return new Scope(); } public static void Capture(NavMeshSurface surface) { if (!((Object)(object)surface == (Object)null) && !Captured.Contains(surface)) { Captured.Add(surface); Log.Debug("Deferred bake: " + ((Object)((Component)surface).gameObject).name + " (agent " + surface.agentTypeID + ")"); } } public static List<NavMeshSurface> Drain() { List<NavMeshSurface> result = new List<NavMeshSurface>(Captured); Captured.Clear(); return result; } public static void Discard() { Captured.Clear(); _depth = 0; } } internal static class FinishSequence { private static RoundManager _roundManager; private static float _deadline; public static bool RpcPending { get; private set; } public static bool HardDeadlinePassed { get { if (RpcPending) { return Time.realtimeSinceStartup > _deadline + 5f; } return false; } } public static void Start(RoundManager roundManager) { _roundManager = roundManager; RpcPending = true; _deadline = Time.realtimeSinceStartup + PluginConfig.MaxFinishSeconds.Value; ((MonoBehaviour)PluginRunner.Instance).StartCoroutine(Run(roundManager)); } private static IEnumerator Run(RoundManager roundManager) { bool gated = PluginConfig.GateRpcOnCompletion.Value; bool navMesh = PluginConfig.NavMeshEnabled.Value && GameRefs.NavMeshAvailable && NavMeshBakeService.ResolveMode() != NavMeshMode.Vanilla; try { RunPhase("ai-nodes", delegate { roundManager.insideAINodes = GameObject.FindGameObjectsWithTag("AINode"); roundManager.allCaveNodes = GameObject.FindGameObjectsWithTag("CaveNode"); roundManager.dungeonCompletedGenerating = true; roundManager.dungeonIsGenerating = false; }); yield return null; RunPhase("bake-dungeon.setup", delegate { if (navMesh) { using (DeferredBakeScope.Begin()) { GameRefs.BakeDunGenNavMesh(roundManager); return; } } GameRefs.BakeDunGenNavMesh(roundManager); }); yield return null; if (navMesh) { IEnumerator drain = Drain("bake-dungeon", gated); while (drain.MoveNext()) { yield return drain.Current; } } RunPhase("cave-door-lights", delegate { GameRefs.SpawnCaveDoorLights(roundManager); }); yield return null; RunPhase("weather", delegate { GameRefs.SetToCurrentLevelWeather(roundManager); }); yield return null; RunPhase("outside-hazards", delegate { if (navMesh) { using (DeferredBakeScope.Begin()) { GameRefs.SpawnOutsideHazards(roundManager); return; } } GameRefs.SpawnOutsideHazards(roundManager); }); yield return null; if (navMesh) { IEnumerator drain = Drain("bake-outside", gated); while (drain.MoveNext()) { yield return drain.Current; } } RunPhase("culling", delegate { Dungeon val = Object.FindObjectOfType<Dungeon>(); if ((Object)(object)val == (Object)null) { Debug.LogError((object)"Error, could not find a Dungeon component!"); } else { AdjacentRoomCullingModified[] array = Object.FindObjectsByType<AdjacentRoomCullingModified>((FindObjectsInactive)1, (FindObjectsSortMode)0); for (int i = 0; i < array.Length; i++) { array[i].SetDungeon(val); } } }); RunPhase("on-finished-event", delegate { GameRefs.InvokeOnFinishedGeneratingDungeon(roundManager); }); } finally { SendRpc("sequence complete"); } } private static IEnumerator Drain(string label, bool gated) { if (!gated) { ((MonoBehaviour)PluginRunner.Instance).StartCoroutine(Guarded(NavMeshBakeService.Drain(label), label)); yield break; } IEnumerator inner = Guarded(NavMeshBakeService.Drain(label), label); while (inner.MoveNext()) { yield return inner.Current; } } private static IEnumerator Guarded(IEnumerator inner, string name) { while (!Expired(name)) { object obj = null; bool flag = false; bool flag2 = false; try { flag = inner.MoveNext(); if (flag) { obj = inner.Current; } } catch (Exception ex) { Log.Error("Phase '" + name + "' threw, continuing anyway: " + ex); flag2 = true; } if (flag2 || !flag) { break; } yield return obj; } } private static void RunPhase(string name, Action body) { if (Expired(name)) { return; } LoadProfiler.BeginPhase(name); try { body(); } catch (Exception ex) { Log.Error("Phase '" + name + "' threw, continuing anyway: " + ex); } finally { LoadProfiler.EndPhase(name); } } private static bool Expired(string name) { if (Time.realtimeSinceStartup <= _deadline) { return false; } Log.Error("Watchdog: " + PluginConfig.MaxFinishSeconds.Value.ToString("F0") + " s exceeded before phase '" + name + "'. Remaining phases are skipped and the RPC is sent anyway so the lobby does not hang. Please report this with the log."); return true; } public static void ForceSendRpc() { SendRpc("safety net, coroutine did not finish"); } private static void SendRpc(string reason) { if (!RpcPending) { return; } RpcPending = false; try { NavMeshBakeService.FlushBlocking("bake-flush"); RoundManager roundManager = _roundManager; if ((Object)(object)roundManager == (Object)null) { Log.Error("RoundManager disappeared before FinishedGeneratingLevelServerRpc could be sent."); return; } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null) { Log.Error("NetworkManager.Singleton is null, cannot send FinishedGeneratingLevelServerRpc."); return; } roundManager.FinishedGeneratingLevelServerRpc(singleton.LocalClientId); Log.Debug("FinishedGeneratingLevelServerRpc sent (" + reason + ")"); } catch (Exception ex) { Log.Error("Failed to send FinishedGeneratingLevelServerRpc: " + ex); } finally { _roundManager = null; LoadProfiler.MarkFinishSequenceDone(); } } } internal static class NavMeshBakeService { public static NavMeshMode ResolveMode() { NavMeshMode value = PluginConfig.NavMeshBakeMode.Value; if (value == NavMeshMode.Async && PluginConfig.RequirePathfindingLibForAsync.Value && !ModCompat.HasPathfindingLib) { return NavMeshMode.Deferred; } return value; } public static IEnumerator Drain(string label) { List<NavMeshSurface> surfaces = DeferredBakeScope.Drain(); if (surfaces.Count == 0) { yield break; } NavMeshMode mode = ResolveMode(); for (int i = 0; i < surfaces.Count; i++) { NavMeshSurface val = surfaces[i]; if ((Object)(object)val == (Object)null) { continue; } string name = label + "." + ((Object)((Component)val).gameObject).name + "[agent " + val.agentTypeID + "]"; if (mode == NavMeshMode.Async) { IEnumerator bake = BakeAsync(val, name); while (bake.MoveNext()) { yield return bake.Current; } } else { BakeBlocking(val, name); yield return null; } } } private static IEnumerator BakeAsync(NavMeshSurface surface, string name) { Stopwatch sync = Stopwatch.StartNew(); NavMeshData val = surface.navMeshData; bool reused = (Object)(object)val != (Object)null; if (!reused) { val = new NavMeshData(surface.agentTypeID) { name = ((Object)((Component)surface).gameObject).name }; } val.position = ((Component)surface).transform.position; val.rotation = ((Component)surface).transform.rotation; if (!reused) { surface.RemoveData(); surface.navMeshData = val; if (((Behaviour)surface).isActiveAndEnabled) { surface.AddData(); } } LoadProfiler.PushLabel(name + " [collect]"); AsyncOperation operation = surface.UpdateNavMesh(val); LoadProfiler.PopLabel(); sync.Stop(); LoadProfiler.PushLabel(name + " [async wait]"); Stopwatch async = Stopwatch.StartNew(); long lastTick = Stopwatch.GetTimestamp(); int frames = 0; double worstFrameMs = 0.0; double blockedMs = 0.0; while (operation != null && !operation.isDone) { yield return null; long timestamp = Stopwatch.GetTimestamp(); double num = (double)(timestamp - lastTick) * 1000.0 / (double)Stopwatch.Frequency; lastTick = timestamp; frames++; if (num > worstFrameMs) { worstFrameMs = num; } if (num > 50.0) { blockedMs += num; } } async.Stop(); LoadProfiler.PopLabel(); double totalMilliseconds = sync.Elapsed.TotalMilliseconds; double totalMilliseconds2 = async.Elapsed.TotalMilliseconds; LoadProfiler.RecordPhase(name, totalMilliseconds + totalMilliseconds2); LoadProfiler.NoteNavMeshBlocking(blockedMs); Log.Info(name + ": " + F(totalMilliseconds) + " ms setup, " + F(totalMilliseconds2) + " ms wait over " + frames + " frame(s), worst frame " + F(worstFrameMs) + " ms, " + F(blockedMs) + " ms actually blocking the main thread" + (reused ? " (reused navmesh data)" : " (fresh navmesh data)")); if (blockedMs > totalMilliseconds2 * 0.5 && totalMilliseconds2 > 100.0) { Log.Warn("NavMesh Async mode is not delivering on this machine: " + F(blockedMs) + " of " + F(totalMilliseconds2) + " ms blocked the main thread. Unity 2022.3 pulls the bake job back onto the main thread when something forces a job-graph sync point. Try NavMesh.Mode = Deferred or Vanilla and compare."); } } public static void FlushBlocking(string label) { List<NavMeshSurface> list = DeferredBakeScope.Drain(); if (list.Count == 0) { return; } Log.Warn(list.Count + " navmesh surface(s) were still pending; baking them synchronously to avoid shipping an incomplete navmesh. Expect a stutter."); for (int i = 0; i < list.Count; i++) { NavMeshSurface val = list[i]; if (!((Object)(object)val == (Object)null)) { BakeBlocking(val, label + "." + ((Object)((Component)val).gameObject).name + "[agent " + val.agentTypeID + "]"); } } } private static void BakeBlocking(NavMeshSurface surface, string name) { LoadProfiler.PushLabel(name + " [blocking]"); Stopwatch stopwatch = Stopwatch.StartNew(); surface.BuildNavMesh(); stopwatch.Stop(); LoadProfiler.PopLabel(); LoadProfiler.RecordPhase(name, stopwatch.Elapsed.TotalMilliseconds); Log.Debug(name + ": " + F(stopwatch.Elapsed.TotalMilliseconds) + " ms (blocking)"); } private static string F(double value) { return value.ToString("F1", CultureInfo.InvariantCulture); } } internal sealed class PluginRunner : MonoBehaviour { private static PluginRunner _instance; public static PluginRunner Instance { get { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected O, but got Unknown //IL_0023: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_instance != (Object)null) { return _instance; } GameObject val = new GameObject("PluginRunner"); Object.DontDestroyOnLoad((Object)val); ((Object)val).hideFlags = (HideFlags)61; _instance = val.AddComponent<PluginRunner>(); return _instance; } } public static void Bootstrap() { _ = Instance; } private void LateUpdate() { LoadProfiler.SampleFrame(); if (FinishSequence.HardDeadlinePassed) { Log.Error("The finish sequence never completed. Sending FinishedGeneratingLevelServerRpc from the safety net so the lobby does not hang. Please report this with the log."); FinishSequence.ForceSendRpc(); } if (LoadProfiler.HasStalled) { Log.Warn("Load never reached FinishGeneratingNewLevelClientRpc; closing the report on a timeout."); LoadProfiler.EndLoad(); } } } } namespace DungeonLoadingOptimizer.Reflection { internal static class GameRefs { private static MethodInfo _bakeDunGenNavMesh; private static MethodInfo _spawnCaveDoorLights; private static MethodInfo _setToCurrentLevelWeather; private static MethodInfo _spawnOutsideHazards; private static Action<RoundManager> _bakeDunGenNavMeshCall; private static Action<RoundManager> _spawnCaveDoorLightsCall; private static Action<RoundManager> _setToCurrentLevelWeatherCall; private static Action<RoundManager> _spawnOutsideHazardsCall; private static FieldRef<RoundManager, Action> _onFinishedGeneratingDungeon; public static MethodInfo ShouldSkipFrame { get; private set; } public static MethodInfo FinishGeneratingLevel { get; private set; } public static MethodInfo BuildNavMesh { get; private set; } public static MethodInfo HasMatchingTagPair { get; private set; } public static bool LifecycleAvailable { get; private set; } public static bool FrameBudgetAvailable { get; private set; } public static bool NavMeshAvailable { get; private set; } public static bool FinishSequenceAvailable { get; private set; } public static bool TagPairCacheAvailable { get; private set; } public static void Resolve() { List<string> list = new List<string>(); ShouldSkipFrame = Method(typeof(DungeonGenerator), "ShouldSkipFrame", list, new Type[1] { typeof(bool) }); FinishGeneratingLevel = Method(typeof(RoundManager), "FinishGeneratingLevel", list); BuildNavMesh = Method(typeof(NavMeshSurface), "BuildNavMesh", list, Type.EmptyTypes); HasMatchingTagPair = Method(typeof(DungeonFlow), "HasMatchingTagPair", list, new Type[2] { typeof(Tile), typeof(Tile) }); _bakeDunGenNavMesh = Method(typeof(RoundManager), "BakeDunGenNavMesh", list); _spawnCaveDoorLights = Method(typeof(RoundManager), "SpawnCaveDoorLights", list); _setToCurrentLevelWeather = Method(typeof(RoundManager), "SetToCurrentLevelWeather", list); _spawnOutsideHazards = Method(typeof(RoundManager), "SpawnOutsideHazards", list); FieldInfo fieldInfo = Field(typeof(RoundManager), "OnFinishedGeneratingDungeon", list); if (fieldInfo != null) { try { _onFinishedGeneratingDungeon = AccessTools.FieldRefAccess<RoundManager, Action>(fieldInfo); } catch (Exception ex) { list.Add("RoundManager.OnFinishedGeneratingDungeon (" + ex.GetType().Name + ")"); } } Field(typeof(RoundManager), "dungeonGenerator", list); Field(typeof(RoundManager), "dungeonCompletedGenerating", list); Field(typeof(RoundManager), "dungeonIsGenerating", list); Field(typeof(RoundManager), "insideAINodes", list); Field(typeof(RoundManager), "allCaveNodes", list); Method(typeof(RoundManager), "FinishedGeneratingLevelServerRpc", list, new Type[1] { typeof(ulong) }); Field(typeof(DungeonGenerator), "GenerateAsynchronously", list); Field(typeof(DungeonGenerator), "MaxAsyncFrameMilliseconds", list); Field(typeof(DungeonGenerator), "PauseBetweenRooms", list); LifecycleAvailable = (Method(typeof(RoundManager), "GenerateNewFloor", list) != null) & (Method(typeof(RoundManager), "FinishGeneratingNewLevelClientRpc", list) != null); FrameBudgetAvailable = ShouldSkipFrame != null; NavMeshAvailable = BuildNavMesh != null && Method(typeof(NavMeshSurface), "UpdateNavMesh", list, new Type[1] { typeof(NavMeshData) }) != null; FinishSequenceAvailable = FinishGeneratingLevel != null && _bakeDunGenNavMesh != null && _spawnCaveDoorLights != null && _setToCurrentLevelWeather != null && _spawnOutsideHazards != null && _onFinishedGeneratingDungeon != null; TagPairCacheAvailable = HasMatchingTagPair != null; if (list.Count > 0) { Log.Error("Game members not found (unsupported game version?): " + string.Join(", ", list.ToArray())); Log.Error("This mod was validated against Lethal Company v81. The affected modules will disable themselves."); } } public static void BakeDunGenNavMesh(RoundManager roundManager) { (_bakeDunGenNavMeshCall ?? (_bakeDunGenNavMeshCall = Bind(_bakeDunGenNavMesh)))(roundManager); } public static void SpawnCaveDoorLights(RoundManager roundManager) { (_spawnCaveDoorLightsCall ?? (_spawnCaveDoorLightsCall = Bind(_spawnCaveDoorLights)))(roundManager); } public static void SetToCurrentLevelWeather(RoundManager roundManager) { (_setToCurrentLevelWeatherCall ?? (_setToCurrentLevelWeatherCall = Bind(_setToCurrentLevelWeather)))(roundManager); } public static void SpawnOutsideHazards(RoundManager roundManager) { (_spawnOutsideHazardsCall ?? (_spawnOutsideHazardsCall = Bind(_spawnOutsideHazards)))(roundManager); } public static void InvokeOnFinishedGeneratingDungeon(RoundManager roundManager) { _onFinishedGeneratingDungeon.Invoke(roundManager)?.Invoke(); } private static Action<RoundManager> Bind(MethodInfo method) { return AccessTools.MethodDelegate<Action<RoundManager>>(method, (object)null, true); } private static MethodInfo Method(Type type, string name, List<string> missing, Type[] parameters = null) { MethodInfo obj = ((parameters == null) ? AccessTools.DeclaredMethod(type, name, (Type[])null, (Type[])null) : AccessTools.DeclaredMethod(type, name, parameters, (Type[])null)); if (obj == null) { missing.Add(type.Name + "." + name + "()"); } return obj; } private static FieldInfo Field(Type type, string name, List<string> missing) { FieldInfo fieldInfo = AccessTools.DeclaredField(type, name); if (fieldInfo == null) { missing.Add(type.Name + "." + name); } return fieldInfo; } } } namespace DungeonLoadingOptimizer.Patches { [HarmonyPatch] internal static class FinishGeneratingLevelPatch { private const string VanillaLabel = "game:FinishGeneratingLevel (incl. blocking navmesh)"; private static bool _labelledVanilla; [HarmonyPatch(typeof(RoundManager), "FinishGeneratingLevel")] [HarmonyPrefix] private static bool FinishGeneratingLevelPrefix(RoundManager __instance) { LoadProfiler.EndGeneration(); if (!PluginConfig.Enabled.Value || !PluginConfig.FinishSequenceEnabled.Value || !GameRefs.FinishSequenceAvailable) { LoadProfiler.BeginPhase("game:FinishGeneratingLevel (incl. blocking navmesh)"); _labelledVanilla = true; return true; } FinishSequence.Start(__instance); return false; } [HarmonyPatch(typeof(RoundManager), "FinishGeneratingLevel")] [HarmonyPostfix] private static void FinishGeneratingLevelPostfix() { if (_labelledVanilla) { _labelledVanilla = false; LoadProfiler.EndPhase("game:FinishGeneratingLevel (incl. blocking navmesh)"); } if (!FinishSequence.RpcPending) { LoadProfiler.MarkFinishSequenceDone(); } } } [HarmonyPatch] internal static class FrameBudgetPatch { private static long _frameEntryTicks; private static int _lastFrame = -1; public static void Reset() { _lastFrame = -1; } [HarmonyPatch(typeof(DungeonGenerator), "ShouldSkipFrame")] [HarmonyPrefix] [HarmonyPriority(800)] private static bool ShouldSkipFramePrefix(DungeonGenerator __instance, bool isRoomPlacement, ref bool __result) { LoadProfiler.NoteWorkUnit(); if (!PluginConfig.Enabled.Value || !PluginConfig.FrameBudgetEnabled.Value) { return true; } if (!__instance.GenerateAsynchronously) { __result = false; return false; } if (isRoomPlacement && __instance.PauseBetweenRooms > 0f) { __result = true; return false; } float num = ((PluginConfig.Mode.Value == BudgetMode.RespectGameValue) ? __instance.MaxAsyncFrameMilliseconds : PluginConfig.EffectiveBudgetMs); if (num <= 0f) { __result = true; return false; } int frameCount = Time.frameCount; if (frameCount != _lastFrame) { _lastFrame = frameCount; _frameEntryTicks = Stopwatch.GetTimestamp(); __result = false; return false; } double num2 = (double)(Stopwatch.GetTimestamp() - _frameEntryTicks) * 1000.0 / (double)Stopwatch.Frequency; __result = num2 >= (double)num; return false; } } [HarmonyPatch] internal static class LoadLifecyclePatch { [HarmonyPatch(typeof(RoundManager), "GenerateNewFloor")] [HarmonyPostfix] private static void GenerateNewFloorPostfix(RoundManager __instance) { Log.Info("Level generation starting."); PatchConflictScanner.ScanOnce(); PluginRunner.Bootstrap(); PostRpcProfilerPatch.Reset(); FrameBudgetPatch.Reset(); DeferredBakeScope.Discard(); TagPairCachePatch.BeginGeneration(__instance); FramerateGovernor.BeginLoad(); LoadProfiler.BeginLoad(__instance); if (Log.IsVerbose && (Object)(object)__instance.dungeonGenerator != (Object)null) { DungeonGenerator generator = __instance.dungeonGenerator.Generator; Log.Debug("Generator configured by the game: GenerateAsynchronously=" + generator.GenerateAsynchronously + " MaxAsyncFrameMilliseconds=" + generator.MaxAsyncFrameMilliseconds + " PauseBetweenRooms=" + generator.PauseBetweenRooms); } } [HarmonyPatch(typeof(RoundManager), "FinishGeneratingNewLevelClientRpc")] [HarmonyPostfix] private static void FinishGeneratingNewLevelClientRpcPostfix() { FramerateGovernor.EndLoad(); TagPairCachePatch.EndGeneration(); LoadProfiler.EndLoad(); } } internal static class FramerateGovernor { private static int _savedTargetFrameRate; private static int _savedVSyncCount; private static bool _active; public static void BeginLoad() { if (!_active && PluginConfig.Enabled.Value && PluginConfig.UncapFramerateDuringLoad.Value) { _savedTargetFrameRate = Application.targetFrameRate; _savedVSyncCount = QualitySettings.vSyncCount; _active = true; Application.targetFrameRate = -1; QualitySettings.vSyncCount = 0; Log.Debug("Framerate uncapped for the duration of the load (was targetFrameRate=" + _savedTargetFrameRate + ", vSyncCount=" + _savedVSyncCount + ")."); } } public static void EndLoad() { if (_active) { _active = false; Application.targetFrameRate = _savedTargetFrameRate; QualitySettings.vSyncCount = _savedVSyncCount; Log.Debug("Framerate settings restored."); } } } [HarmonyPatch] internal static class NavMeshBuildInterceptPatch { [HarmonyPatch(typeof(NavMeshSurface), "BuildNavMesh")] [HarmonyPrefix] [HarmonyPriority(800)] private static bool BuildNavMeshPrefix(NavMeshSurface __instance) { if (!DeferredBakeScope.Capturing) { return true; } DeferredBakeScope.Capture(__instance); return false; } } internal static class PostRpcProfilerPatch { private struct Entry { public string Name; public long StartTicks; public int Depth; } private static readonly string[] TargetNames = new string[11] { "SpawnSyncedProps", "GeneratedFloorPostProcessing", "SpawnScrapInLevel", "SpawnMapObjects", "RefreshEnemyVents", "FinishGeneratingNewLevelClientRpc", "RefreshLightsList", "RefreshEnemiesList", "ResetEnemyTypesSpawnedCounts", "PredictAllOutsideEnemies", "SetLevelObjectVariables" }; private static readonly Stack<Entry> Open = new Stack<Entry>(); public static bool ShouldInstrument { get { if (PluginConfig.ProfileLoads.Value) { return PluginConfig.LogLevel.Value != LogVerbosity.Off; } return false; } } public static void Reset() { Open.Clear(); } public static void Apply(Harmony harmony) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Expected O, but got Unknown //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Expected O, but got Unknown HarmonyMethod val = new HarmonyMethod(AccessTools.DeclaredMethod(typeof(PostRpcProfilerPatch), "Enter", (Type[])null, (Type[])null)) { priority = 800 }; HarmonyMethod val2 = new HarmonyMethod(AccessTools.DeclaredMethod(typeof(PostRpcProfilerPatch), "Exit", (Type[])null, (Type[])null)) { priority = 0 }; int num = 0; string[] targetNames = TargetNames; foreach (string text in targetNames) { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(RoundManager), text, (Type[])null, (Type[])null); if (methodInfo == null) { Log.Debug("Post-RPC profiler: RoundManager." + text + " not found, skipped."); continue; } try { harmony.Patch((MethodBase)methodInfo, val, val2, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); num++; } catch (Exception ex) { Log.Warn("Post-RPC profiler could not instrument RoundManager." + text + ": " + ex.Message); } } Log.Debug("Post-RPC profiler instrumenting " + num + "/" + TargetNames.Length + " method(s)."); } private static void Enter(MethodBase __originalMethod) { if (LoadProfiler.IsLoading) { Open.Push(new Entry { Name = __originalMethod.Name, StartTicks = Stopwatch.GetTimestamp(), Depth = Open.Count }); LoadProfiler.PushLabel("game:" + __originalMethod.Name); } } private static void Exit(MethodBase __originalMethod) { if (!LoadProfiler.IsLoading || Open.Count == 0) { return; } while (Open.Count > 0) { Entry entry = Open.Pop(); double ms = (double)(Stopwatch.GetTimestamp() - entry.StartTicks) * 1000.0 / (double)Stopwatch.Frequency; LoadProfiler.RecordPhase(new string(' ', entry.Depth * 2) + "game:" + entry.Name, ms); LoadProfiler.PopLabel(); if (entry.Name == __originalMethod.Name) { break; } } } } [HarmonyPatch] internal static class TagPairCachePatch { private readonly struct Key { private readonly Tile _a; private readonly Tile _b; public Key(Tile a, Tile b) { _a = a; _b = b; } public override int GetHashCode() { return ((((Object)(object)_a != (Object)null) ? ((Object)_a).GetInstanceID() : 0) * 397) ^ (((Object)(object)_b != (Object)null) ? ((Object)_b).GetInstanceID() : 0); } public override bool Equals(object obj) { if (obj is Key key && (Object)(object)key._a == (Object)(object)_a) { return (Object)(object)key._b == (Object)(object)_b; } return false; } } private static readonly Dictionary<Key, bool> Cache = new Dictionary<Key, bool>(); private static bool _active; public static void BeginGeneration(RoundManager roundManager) { Cache.Clear(); _active = false; if (PluginConfig.Enabled.Value && PluginConfig.TagPairCacheEnabled.Value && GameRefs.TagPairCacheAvailable) { DungeonFlow val = (((Object)(object)roundManager != (Object)null && (Object)(object)roundManager.dungeonGenerator != (Object)null) ? roundManager.dungeonGenerator.Generator.DungeonFlow : null); int num = (((Object)(object)val != (Object)null && val.TileConnectionTags != null) ? val.TileConnectionTags.Count : 0); _active = num > 0; Log.Debug("Tag pair cache: TileConnectionTags=" + num + " -> " + (_active ? "active" : "skipped")); } } public static void EndGeneration() { Cache.Clear(); _active = false; } [HarmonyPatch(typeof(DungeonFlow), "HasMatchingTagPair")] [HarmonyPrefix] private static bool HasMatchingTagPairPrefix(Tile a, Tile b, ref bool __result) { if (!_active) { return true; } if (!Cache.TryGetValue(new Key(a, b), out var value)) { return true; } __result = value; return false; } [HarmonyPatch(typeof(DungeonFlow), "HasMatchingTagPair")] [HarmonyPostfix] private static void HasMatchingTagPairPostfix(Tile a, Tile b, bool __result) { if (_active) { Cache[new Key(a, b)] = __result; } } } } namespace DungeonLoadingOptimizer.Diagnostics { internal sealed class FrameTimeMonitor { private readonly List<float> _samples = new List<float>(1024); private float _thresholdMs; public float MaxMs { get; private set; } public string MaxLabel { get; private set; } = "?"; public int OverThresholdCount { get; private set; } public int SampleCount => _samples.Count; public void Reset(float thresholdMs) { _samples.Clear(); _thresholdMs = thresholdMs; MaxMs = 0f; MaxLabel = "?"; OverThresholdCount = 0; } public void Sample(string label) { float num = Time.unscaledDeltaTime * 1000f; _samples.Add(num); if (num > MaxMs) { MaxMs = num; MaxLabel = label; } if (num > _thresholdMs) { OverThresholdCount++; } } public float? Percentile(float p) { if (_samples.Count < Mathf.CeilToInt(1f / (1f - p))) { return null; } float[] array = _samples.ToArray(); Array.Sort(array); int num = Mathf.Clamp(Mathf.CeilToInt(p * (float)array.Length) - 1, 0, array.Length - 1); return array[num]; } } internal static class LayoutHasher { private const uint FnvOffsetBasis = 2166136261u; private const uint FnvPrime = 16777619u; public static string Hash(Dungeon dungeon, out int tileCount) { //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) tileCount = 0; if ((Object)(object)dungeon == (Object)null) { return "n/a"; } ReadOnlyCollection<Tile> allTiles = dungeon.AllTiles; if (allTiles == null) { return "n/a"; } uint hash = 2166136261u; for (int i = 0; i < allTiles.Count; i++) { Tile val = allTiles[i]; if (!((Object)(object)val == (Object)null)) { tileCount++; string value = (((Object)(object)val.Prefab != (Object)null) ? ((Object)val.Prefab).name : "<null>"); hash = Combine(hash, value); Transform transform = ((Component)val).transform; hash = Combine(hash, Quantize(transform.position.x)); hash = Combine(hash, Quantize(transform.position.y)); hash = Combine(hash, Quantize(transform.position.z)); Quaternion rotation = transform.rotation; Vector3 eulerAngles = ((Quaternion)(ref rotation)).eulerAngles; hash = Combine(hash, Quantize(eulerAngles.x)); hash = Combine(hash, Quantize(eulerAngles.y)); hash = Combine(hash, Quantize(eulerAngles.z)); hash = Combine(hash, i); } } hash = Combine(hash, (dungeon.Doors != null) ? dungeon.Doors.Count : (-1)); hash = Combine(hash, (dungeon.MainPathTiles != null) ? dungeon.MainPathTiles.Count : (-1)); return Combine(hash, (dungeon.BranchPathTiles != null) ? dungeon.BranchPathTiles.Count : (-1)).ToString("X8"); } private static int Quantize(float value) { return Mathf.RoundToInt(value * 100f); } private static uint Combine(uint hash, string value) { for (int i = 0; i < value.Length; i++) { hash ^= value[i]; hash *= 16777619; } return hash; } private static uint Combine(uint hash, int value) { for (int i = 0; i < 32; i += 8) { hash ^= (uint)((value >> i) & 0xFF); hash *= 16777619; } return hash; } } internal static class LoadProfiler { private struct Phase { public string Name; public double Ms; } private static readonly Stopwatch Total = new Stopwatch(); private static readonly Stopwatch Generation = new Stopwatch(); private static readonly Stopwatch PhaseTimer = new Stopwatch(); private static readonly List<Phase> Phases = new List<Phase>(); private static readonly FrameTimeMonitor Frames = new FrameTimeMonitor(); private static DungeonGenerator _generator; private static RoundManager _roundManager; private static string _moon; private static int _workUnits; private static int _generationFrames; private static int _lastCountedFrame = -1; private static bool _generationDone; private static string _currentPhase = "finish"; private static readonly List<string> _labels = new List<string>(); private static double _navMeshBlockedMs; private static int _attributionFrame = -1; private static string _attributionName; private static double _attributionMs; public static bool IsLoading { get; private set; } private static string CurrentLabel { get { if (!_generationDone) { return "generation"; } if (_attributionFrame == Time.frameCount && _attributionName != null) { return _attributionName; } if (_labels.Count > 0) { return _labels[_labels.Count - 1]; } return _currentPhase; } } public static bool HasStalled { get { if (IsLoading) { return Total.Elapsed.TotalSeconds > 120.0; } return false; } } public static void BeginLoad(RoundManager roundManager) { if (PluginConfig.ProfileLoads.Value) { _roundManager = roundManager; _generator = (((Object)(object)roundManager != (Object)null && (Object)(object)roundManager.dungeonGenerator != (Object)null) ? roundManager.dungeonGenerator.Generator : null); _moon = (((Object)(object)roundManager != (Object)null && (Object)(object)roundManager.currentLevel != (Object)null) ? roundManager.currentLevel.PlanetName : "?"); _workUnits = 0; _generationFrames = 0; _lastCountedFrame = -1; _generationDone = false; _navMeshBlockedMs = 0.0; _labels.Clear(); Phases.Clear(); Frames.Reset(PluginConfig.FrameSpikeThresholdMs.Value); Total.Restart(); Generation.Restart(); IsLoading = true; } } public static void NoteWorkUnit() { if (IsLoading) { _workUnits++; int frameCount = Time.frameCount; if (frameCount != _lastCountedFrame) { _lastCountedFrame = frameCount; _generationFrames++; } } } public static void EndGeneration() { if (IsLoading && !_generationDone) { Generation.Stop(); _generationDone = true; } } public static void SampleFrame() { if (IsLoading) { Frames.Sample(CurrentLabel); } } private static void NoteFrameAttribution(string name, double ms) { int frameCount = Time.frameCount; if (frameCount != _attributionFrame) { _attributionFrame = frameCount; _attributionName = null; _attributionMs = 0.0; } if (!(ms <= _attributionMs)) { _attributionMs = ms; _attributionName = name.Trim(); } } public static void PushLabel(string name) { if (IsLoading) { _labels.Add(name); } } public static void PopLabel() { if (_labels.Count > 0) { _labels.RemoveAt(_labels.Count - 1); } } public static void BeginPhase(string name) { PushLabel(name); PhaseTimer.Restart(); } public static void EndPhase(string name) { PopLabel(); if (IsLoading) { PhaseTimer.Stop(); double totalMilliseconds = PhaseTimer.Elapsed.TotalMilliseconds; Phases.Add(new Phase { Name = name, Ms = totalMilliseconds }); NoteFrameAttribution(name, totalMilliseconds); } } public static void RecordPhase(string name, double ms) { if (IsLoading) { Phases.Add(new Phase { Name = name, Ms = ms }); NoteFrameAttribution(name, ms); } } public static void NoteNavMeshBlocking(double ms) { if (IsLoading) { _navMeshBlockedMs += ms; } } public static void MarkFinishSequenceDone() { if (IsLoading) { EndGeneration(); _currentPhase = "post-rpc (game spawns props/scrap)"; } } public static void EndLoad() { if (IsLoading) { IsLoading = false; Total.Stop(); EndGeneration(); if (PluginConfig.ProfileLoads.Value) { Log.Info(BuildReport()); } } } private static string BuildReport() { CultureInfo invariantCulture = CultureInfo.InvariantCulture; StringBuilder stringBuilder = new StringBuilder(); string text = ((_generator != null) ? _generator.ChosenSeed.ToString(invariantCulture) : "?"); string text2 = ((_generator != null && (Object)(object)_generator.DungeonFlow != (Object)null) ? ((Object)_generator.DungeonFlow).name : "?"); stringBuilder.AppendLine(); stringBuilder.AppendLine("=== LOAD REPORT moon=" + _moon + " seed=" + text + " flow=" + text2 + " ==="); stringBuilder.AppendLine(" modules : " + ModuleSummary()); double totalMilliseconds = Generation.Elapsed.TotalMilliseconds; double value = ((_generationFrames > 0) ? ((double)_workUnits / (double)_generationFrames) : 0.0); double value2 = ((_workUnits > 0) ? (totalMilliseconds / (double)_workUnits) : 0.0); stringBuilder.AppendLine(" generation : " + F(totalMilliseconds / 1000.0, 2) + " s over " + _generationFrames + " frames | " + _workUnits + " work units | " + F(value, 2) + " units/frame | " + F(value2, 2) + " ms/unit (wall clock, includes rendering)"); if (Phases.Count > 0) { double num = 0.0; foreach (Phase phase in Phases) { num += phase.Ms; } int num2 = 0; foreach (Phase phase2 in Phases) { num2 = Math.Max(num2, phase2.Name.Length); } stringBuilder.AppendLine(" finish : total " + F(num / 1000.0, 2) + " s"); foreach (Phase phase3 in Phases) { stringBuilder.AppendLine(" " + phase3.Name.PadRight(num2 + 2) + F(phase3.Ms, 1).PadLeft(8) + " ms"); } } if (_navMeshBlockedMs > 0.0) { stringBuilder.AppendLine(" navmesh blocking the main thread despite Async mode : " + F(_navMeshBlockedMs, 1) + " ms"); } float? num3 = Frames.Percentile(0.99f); stringBuilder.AppendLine(" frames during load : n=" + Frames.SampleCount + " max " + F(Frames.MaxMs, 1) + " ms (during " + Frames.MaxLabel + ") p99 " + (num3.HasValue ? (F(num3.Value, 1) + " ms") : "n/a (too few frames)") + " >" + F(PluginConfig.FrameSpikeThresholdMs.Value, 0) + "ms: " + Frames.OverThresholdCount); if (PluginConfig.LogLayoutHash.Value) { if (_generator != null) { int tileCount; string text3 = LayoutHasher.Hash(_generator.CurrentDungeon, out tileCount); stringBuilder.AppendLine(" layout hash : " + text3 + " (tiles=" + tileCount + ")"); } int sampled; string text4 = NavMeshHasher.Hash(_roundManager, out sampled); stringBuilder.AppendLine(" navmesh hash : " + text4 + " (probes=" + sampled + ")"); } return stringBuilder.ToString().TrimEnd(); } private static string ModuleSummary() { if (!PluginConfig.Enabled.Value) { return "ALL DISABLED (control)"; } string text = (PluginConfig.FrameBudgetEnabled.Value ? ("budget=ON(" + F(PluginConfig.EffectiveBudgetMs, 1) + "ms," + PluginConfig.Mode.Value.ToString() + ")") : "budget=OFF"); string text2 = (PluginConfig.NavMeshEnabled.Value ? ("navmesh=" + NavMeshBakeService.ResolveMode().ToString() + (ModCompat.HasPathfindingLib ? "(PathfindingLib)" : "(no PathfindingLib)")) : "navmesh=OFF"); string text3 = (PluginConfig.FinishSequenceEnabled.Value ? ("finish=ON" + (PluginConfig.GateRpcOnCompletion.Value ? "(gated)" : "(ungated)")) : "finish=OFF"); return text + " " + text2 + " " + text3; } private static string F(double value, int decimals) { return value.ToString("F" + decimals, CultureInfo.InvariantCulture); } } internal static class NavMeshHasher { private const uint FnvOffsetBasis = 2166136261u; private const uint FnvPrime = 16777619u; private const float SampleRadius = 8f; public static string Hash(RoundManager roundManager, out int sampled) { sampled = 0; if ((Object)(object)roundManager == (Object)null) { return "n/a"; } uint hash = 2166136261u; hash = HashNodes(hash, roundManager.insideAINodes, ref sampled); hash = HashNodes(hash, roundManager.allCaveNodes, ref sampled); hash = HashNodes(hash, roundManager.outsideAINodes, ref sampled); if (sampled != 0) { return hash.ToString("X8"); } return "n/a"; } private static uint HashNodes(uint hash, GameObject[] nodes, ref int sampled) { //IL_0028: 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_006c: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) if (nodes == null) { return hash; } NavMeshHit val2 = default(NavMeshHit); foreach (GameObject val in nodes) { if (!((Object)(object)val == (Object)null)) { sampled++; bool flag = NavMesh.SamplePosition(val.transform.position, ref val2, 8f, -1); hash = Combine(hash, flag ? 1 : 0); if (flag) { hash = Combine(hash, Mathf.RoundToInt(((NavMeshHit)(ref val2)).position.x * 10f)); hash = Combine(hash, Mathf.RoundToInt(((NavMeshHit)(ref val2)).position.y * 10f)); hash = Combine(hash, Mathf.RoundToInt(((NavMeshHit)(ref val2)).position.z * 10f)); hash = Combine(hash, ((NavMeshHit)(ref val2)).mask); } } } return hash; } private static uint Combine(uint hash, int value) { for (int i = 0; i < 32; i += 8) { hash ^= (uint)((value >> i) & 0xFF); hash *= 16777619; } return hash; } } } namespace DungeonLoadingOptimizer.Config { internal enum LogVerbosity { Off, Summary, Verbose } internal enum BudgetPreset { FpsPriority, Balanced, Speed, Custom } internal enum BudgetMode { Override, RespectGameValue } internal enum NavMeshMode { Async, Deferred, Vanilla } internal static class PluginConfig { public static ConfigEntry<bool> Enabled; public static ConfigEntry<LogVerbosity> LogLevel; public static ConfigEntry<bool> FrameBudgetEnabled; public static ConfigEntry<BudgetPreset> Preset; public static ConfigEntry<float> FrameBudgetMs; public static ConfigEntry<BudgetMode> Mode; public static ConfigEntry<bool> NavMeshEnabled; public static ConfigEntry<NavMeshMode> NavMeshBakeMode; public static ConfigEntry<bool> RequirePathfindingLibForAsync; public static ConfigEntry<bool> FinishSequenceEnabled; public static ConfigEntry<bool> GateRpcOnCompletion; public static ConfigEntry<float> MaxFinishSeconds; public static ConfigEntry<bool> TagPairCacheEnabled; public static ConfigEntry<bool> ProfileLoads; public static ConfigEntry<float> FrameSpikeThresholdMs; public static ConfigEntry<bool> LogLayoutHash; public static ConfigEntry<bool> UncapFramerateDuringLoad; public static float EffectiveBudgetMs => Preset.Value switch { BudgetPreset.FpsPriority => 2f, BudgetPreset.Speed => 12f, BudgetPreset.Custom => FrameBudgetMs.Value, _ => 6f, }; public static void BindAll(ConfigFile cfg) { //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Expected O, but got Unknown //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Expected O, but got Unknown //IL_01dd: Unknown result type (might be due to invalid IL or missing references) //IL_01e7: Expected O, but got Unknown Enabled = cfg.Bind<bool>("General", "Enabled", true, "Master switch. Setting this to false disables every patch but keeps the diagnostics running, which makes it the control case for comparing with and without the mod."); LogLevel = cfg.Bind<LogVerbosity>("General", "LogLevel", LogVerbosity.Summary, "Off = silent. Summary = one report per load. Verbose = per-phase and per-surface detail."); FrameBudgetEnabled = cfg.Bind<bool>("FrameBudget", "Enabled", true, "Fixes DunGen's frame budget. The game measures wall-clock time: its stopwatch keeps running through rendering, so the 1 ms budget it configures is already spent by the time the coroutine resumes, and generation is limited to one work unit per frame. This module counts actual work time instead. By far the highest-value module."); Preset = cfg.Bind<BudgetPreset>("FrameBudget", "Preset", BudgetPreset.Balanced, "FpsPriority = 2 ms, Balanced = 6 ms, Speed = 12 ms, Custom = FrameBudgetMs. A higher budget means shorter loads and lower framerate while loading."); FrameBudgetMs = cfg.Bind<float>("FrameBudget", "FrameBudgetMs", 6f, new ConfigDescription("Per-frame work budget in milliseconds. Only used when Preset = Custom.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 100f), Array.Empty<object>())); Mode = cfg.Bind<BudgetMode>("FrameBudget", "BudgetMode", BudgetMode.Override, "Override = impose the preset's budget. RespectGameValue = keep the game's value (1 ms) and only fix the time accounting; loading then stays noticeably slower."); NavMeshEnabled = cfg.Bind<bool>("NavMesh", "Enabled", true, "Stops the navmesh bake from blocking the main thread. That block is what cuts outgoing voice, since Dissonance captures the microphone on the main thread."); NavMeshBakeMode = cfg.Bind<NavMeshMode>("NavMesh", "Mode", NavMeshMode.Async, "Async = voxelization moves to worker threads (recommended). Deferred = blocking bake, but one surface per frame; the safe fallback. Vanilla = leave it alone, the A/B control."); RequirePathfindingLibForAsync = cfg.Bind<bool>("NavMesh", "RequirePathfindingLibForAsync", true, "Automatically fall back from Async to Deferred when PathfindingLib is absent. PathfindingLib holds its NavMeshLock in write mode for the whole async operation, which is exactly what keeps the job off the main thread. Only turn this off if you know what you are doing."); FinishSequenceEnabled = cfg.Bind<bool>("FinishSequence", "Enabled", true, "Spreads RoundManager.FinishGeneratingLevel over several frames instead of one. Required for the NavMesh module to be able to wait for bakes to finish."); GateRpcOnCompletion = cfg.Bind<bool>("FinishSequence", "GateRpcOnCompletion", true, "Only send FinishedGeneratingLevelServerRpc once the navmesh bakes are done, so scrap and enemies never spawn onto an incomplete navmesh. Setting this to false sends the RPC at its stock position: useful to isolate a problem, but it reintroduces that risk."); MaxFinishSeconds = cfg.Bind<float>("FinishSequence", "MaxFinishSeconds", 20f, new ConfigDescription("Watchdog. Past this deadline the sequence abandons the remaining phases and sends the RPC anyway. The game's LoadNewLevelWait has no timeout of its own: without this, a phase that throws would hang the lobby forever.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 120f), Array.Empty<object>())); TagPairCacheEnabled = cfg.Bind<bool>("TagPairCache", "Enabled", false, "Memoizes DungeonFlow.HasMatchingTagPair. No effect on vanilla interiors, which do not use TileConnectionTags: the module then disables itself. It can only help on some custom interiors."); ProfileLoads = cfg.Bind<bool>("Diagnostics", "ProfileLoads", true, "Writes a numeric report for every load. Runs even when General.Enabled = false, so it can serve as the control."); FrameSpikeThresholdMs = cfg.Bind<float>("Diagnostics", "FrameSpikeThresholdMs", 100f, new ConfigDescription("Long-frame counting threshold. The number of frames above this threshold is the proxy metric for voice chat health during loading.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(16f, 2000f), Array.Empty<object>())); LogLayoutHash = cfg.Bind<bool>("Diagnostics", "LogLayoutHash", true, "Logs a fingerprint of the generated dungeon. Two clients in the same lobby must print the same fingerprint: this is the determinism regression test."); UncapFramerateDuringLoad = cfg.Bind<bool>("Experimental", "UncapFramerateDuringLoad", false, "Removes the framerate cap during generation. More frames per second means more work units per second and more microphone capture passes. The original setting is restored afterwards."); } } } namespace DungeonLoadingOptimizer.Compat { internal static class ModCompat { public const string PathfindingLibGuid = "Zaggy1024.PathfindingLib"; public const string PathfindingLagFixGuid = "Zaggy1024.PathfindingLagFix"; public const string LethalLevelLoaderGuid = "imabatby.lethallevelloader"; public const string LethalPerformanceGuid = "LethalPerformance"; public const string LethalConfigGuid = "ainavt.lc.lethalconfig"; public static bool HasPathfindingLib { get; private set; } public static bool HasLethalLevelLoader { get; private set; } public static bool HasLethalConfig { get; private set; } public static void Detect() { HasPathfindingLib = IsLoaded("Zaggy1024.PathfindingLib"); HasLethalLevelLoader = IsLoaded("imabatby.lethallevelloader"); HasLethalConfig = IsLoaded("ainavt.lc.lethalconfig"); Log.Info("Compat: PathfindingLib=" + Yn(HasPathfindingLib) + " PathfindingLagFix=" + Yn(IsLoaded("Zaggy1024.PathfindingLagFix")) + " LethalLevelLoader=" + Yn(HasLethalLevelLoader) + " LethalPerformance=" + Yn(IsLoaded("LethalPerformance")) + " LethalConfig=" + Yn(HasLethalConfig)); } public static bool IsLoaded(string guid) { return Chainloader.PluginInfos.ContainsKey(guid); } private static string Yn(bool value) { if (!value) { return "no"; } return "yes"; } } internal static class PatchConflictScanner { private static readonly Dictionary<string, string> Expected = new Dictionary<string, string> { { "Zaggy1024.PathfindingLib|BuildNavMesh", "NavMeshLock wrapping, expected" } }; private static bool _scanned; public static void ScanOnce() { if (!_scanned) { _scanned = true; Scan(); } } public static void Scan() { List<KeyValuePair<string, MethodBase>> obj = new List<KeyValuePair<string, MethodBase>> { new KeyValuePair<string, MethodBase>("DungeonGenerator.ShouldSkipFrame", GameRefs.ShouldSkipFrame), new KeyValuePair<string, MethodBase>("RoundManager.FinishGeneratingLevel", GameRefs.FinishGeneratingLevel), new KeyValuePair<string, MethodBase>("NavMeshSurface.BuildNavMesh", GameRefs.BuildNavMesh), new KeyValuePair<string, MethodBase>("DungeonFlow.HasMatchingTagPair", GameRefs.HasMatchingTagPair) }; int num = 0; foreach (KeyValuePair<string, MethodBase> item in obj) { if (item.Value == null) { continue; } Patches patchInfo = Harmony.GetPatchInfo(item.Value); if (patchInfo != null) { StringBuilder stringBuilder = new StringBuilder(); num += Describe(stringBuilder, item.Key, "prefix", patchInfo.Prefixes); num += Describe(stringBuilder, item.Key, "postfix", patchInfo.Postfixes); num += Describe(stringBuilder, item.Key, "transpiler", patchInfo.Transpilers); num += Describe(stringBuilder, item.Key, "finalizer", patchInfo.Finalizers); if (stringBuilder.Length > 0) { Log.Warn(stringBuilder.ToString().TrimEnd()); } } } if (num == 0) { Log.Info("No unexpected foreign patches on our targets."); } } private static int Describe(StringBuilder report, string targetName, string kind, IEnumerable<Patch> patches) { int num = 0; string text = targetName.Substring(targetName.IndexOf('.') + 1); foreach (Patch patch in patches) { if (!(patch.owner == "RustyAstroboy.DungeonLoadingOptimizer")) { if (Expected.TryGetValue(patch.owner + "|" + text, out var value)) { Log.Debug("Foreign patch on " + targetName + ": " + kind + " from " + patch.owner + " (" + value + ")"); } else { num++; report.AppendLine("Foreign patch on " + targetName + ": " + kind + " from '" + patch.owner + "' (priority " + patch.priority + "). If DungeonLoadingOptimizer misbehaves, that is the first suspect."); } } } return num; } } }