using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using LethalConfig; using LethalConfig.ConfigItems; using LethalConfig.ConfigItems.Options; using Unity.Netcode; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("BM_CorpseCleaner")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("BM_CorpseCleaner")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("cbac02e1-4c13-47a9-a908-a7d3d8b6229c")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] namespace BM_CorpseCleaner; internal static class Configuration { internal static ConfigEntry MaximumCorpses; internal static ConfigEntry MinimumCorpseLifetime; internal static void Initialize(ConfigFile config) { MaximumCorpses = config.Bind("Corpse Cleanup", "Maximum Enemy Corpses", 20, "Maximum number of enemy corpses allowed to remain. When the limit is exceeded, the oldest eligible corpse is removed first."); MinimumCorpseLifetime = config.Bind("Corpse Cleanup", "Minimum Corpse Lifetime", 30f, "Minimum number of seconds an enemy corpse must remain before it may be removed."); RegisterLethalConfig(); } private static void RegisterLethalConfig() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected O, but got Unknown //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown //IL_0045: 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_0055: Expected O, but got Unknown //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Expected O, but got Unknown //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Expected O, but got Unknown LethalConfigManager.SetModDescription("Limits the number of enemy corpses kept in the level. Oldest eligible enemy corpses are removed first. Player corpses are never affected."); LethalConfigManager.SkipAutoGen(); ConfigEntry maximumCorpses = MaximumCorpses; IntSliderOptions val = new IntSliderOptions(); ((BaseRangeOptions)val).Min = 1; ((BaseRangeOptions)val).Max = 100; ((BaseOptions)val).RequiresRestart = false; LethalConfigManager.AddConfigItem((BaseConfigItem)new IntSliderConfigItem(maximumCorpses, val)); ConfigEntry minimumCorpseLifetime = MinimumCorpseLifetime; FloatSliderOptions val2 = new FloatSliderOptions(); ((BaseRangeOptions)val2).Min = 0f; ((BaseRangeOptions)val2).Max = 600f; ((BaseOptions)val2).RequiresRestart = false; LethalConfigManager.AddConfigItem((BaseConfigItem)new FloatSliderConfigItem(minimumCorpseLifetime, val2)); } } internal static class CorpseManager { private sealed class CorpseEntry { public EnemyAI Enemy; public float DeathTime; public long Sequence; } [HarmonyPatch(typeof(StartOfRound), "Start")] private static class StartOfRoundStartPatch { [HarmonyPostfix] private static void Postfix(StartOfRound __instance) { if (!((Object)(object)__instance == (Object)null)) { BeginSession(__instance); } } } [HarmonyPatch(typeof(EnemyAI), "KillEnemy")] private static class EnemyAIKillEnemyPatch { [HarmonyPostfix] private static void Postfix(EnemyAI __instance) { if (!((Object)(object)__instance == (Object)null)) { RegisterDeath(__instance); } } } private static readonly List corpses = new List(); private static long nextSequence; private static Coroutine cleanupCoroutine; private static StartOfRound activeRound; private static readonly WaitForSecondsRealtime CleanupDelay = new WaitForSecondsRealtime(1f); internal static void Initialize() { corpses.Clear(); nextSequence = 0L; cleanupCoroutine = null; activeRound = null; Plugin.Log.LogInfo((object)"Corpse manager initialized."); } internal static void BeginSession(StartOfRound round) { if ((Object)(object)round == (Object)null) { return; } if (!IsServer()) { Plugin.Log.LogInfo((object)"BM_CorpseCleaner running as client. Corpse cleanup will be handled by server."); return; } corpses.Clear(); nextSequence = 0L; activeRound = round; if (cleanupCoroutine != null) { try { ((MonoBehaviour)round).StopCoroutine(cleanupCoroutine); } catch { } cleanupCoroutine = null; } ScanExistingDeadEnemies(); cleanupCoroutine = ((MonoBehaviour)round).StartCoroutine(CleanupLoop(round)); Plugin.Log.LogWarning((object)"### BM_CORPSECLEANER SERVER ACTIVE ###"); } internal static void RegisterDeath(EnemyAI enemy) { if (IsServer() && !((Object)(object)enemy == (Object)null) && enemy.isEnemyDead && !ContainsEnemy(enemy)) { CorpseEntry corpseEntry = new CorpseEntry(); corpseEntry.Enemy = enemy; corpseEntry.DeathTime = Time.realtimeSinceStartup; corpseEntry.Sequence = nextSequence++; corpses.Add(corpseEntry); Plugin.Log.LogInfo((object)("Registered enemy corpse #" + corpseEntry.Sequence + ": " + GetEnemyName(enemy) + " | tracked=" + corpses.Count)); TryCleanup(); } } private static IEnumerator CleanupLoop(StartOfRound round) { Plugin.Log.LogInfo((object)"BM corpse cleanup loop started."); while ((Object)(object)round != (Object)null && (Object)(object)round == (Object)(object)activeRound) { if (IsServer()) { ScanExistingDeadEnemies(); RemoveInvalidEntries(); TryCleanup(); } yield return CleanupDelay; } cleanupCoroutine = null; Plugin.Log.LogInfo((object)"BM corpse cleanup loop ended."); } private static void TryCleanup() { if (!IsServer()) { return; } RemoveInvalidEntries(); int num = Configuration.MaximumCorpses.Value; if (num < 1) { num = 1; } while (CountTrackedCorpses() > num) { CorpseEntry corpseEntry = FindOldestEligibleCorpse(); if (corpseEntry == null) { break; } if (!RemoveCorpse(corpseEntry)) { corpses.Remove(corpseEntry); } } } private static CorpseEntry FindOldestEligibleCorpse() { float realtimeSinceStartup = Time.realtimeSinceStartup; float num = Mathf.Max(0f, Configuration.MinimumCorpseLifetime.Value); CorpseEntry corpseEntry = null; foreach (CorpseEntry corpse in corpses) { if (corpse == null) { continue; } EnemyAI enemy = corpse.Enemy; if ((Object)(object)enemy == (Object)null || !enemy.isEnemyDead) { continue; } float num2 = realtimeSinceStartup - corpse.DeathTime; if (!(num2 < num) && !((Object)(object)enemy.enemyType == (Object)null) && enemy.enemyType.canBeDestroyed) { NetworkObject networkObject = ((NetworkBehaviour)enemy).NetworkObject; if (!((Object)(object)networkObject == (Object)null) && networkObject.IsSpawned && (corpseEntry == null || corpse.Sequence < corpseEntry.Sequence)) { corpseEntry = corpse; } } } return corpseEntry; } private static bool RemoveCorpse(CorpseEntry entry) { if (entry == null || (Object)(object)entry.Enemy == (Object)null) { return false; } EnemyAI enemy = entry.Enemy; if (!enemy.isEnemyDead) { return false; } if ((Object)(object)enemy.enemyType == (Object)null || !enemy.enemyType.canBeDestroyed) { return false; } NetworkObject networkObject = ((NetworkBehaviour)enemy).NetworkObject; if ((Object)(object)networkObject == (Object)null || !networkObject.IsSpawned) { return false; } float num = Time.realtimeSinceStartup - entry.DeathTime; Plugin.Log.LogInfo((object)("Removing oldest enemy corpse #" + entry.Sequence + ": " + GetEnemyName(enemy) + " | age=" + num.ToString("0.0") + "s")); networkObject.Despawn(true); corpses.Remove(entry); Plugin.Log.LogInfo((object)("Enemy corpse removed. Remaining tracked corpses=" + CountTrackedCorpses())); return true; } private static void ScanExistingDeadEnemies() { if ((Object)(object)RoundManager.Instance == (Object)null || RoundManager.Instance.SpawnedEnemies == null) { return; } List spawnedEnemies = RoundManager.Instance.SpawnedEnemies; foreach (EnemyAI item in spawnedEnemies) { if (!((Object)(object)item == (Object)null) && item.isEnemyDead && !ContainsEnemy(item)) { CorpseEntry corpseEntry = new CorpseEntry(); corpseEntry.Enemy = item; corpseEntry.DeathTime = Time.realtimeSinceStartup; corpseEntry.Sequence = nextSequence++; corpses.Add(corpseEntry); Plugin.Log.LogDebug((object)("Discovered existing dead enemy #" + corpseEntry.Sequence + ": " + GetEnemyName(item))); } } } private static void RemoveInvalidEntries() { for (int num = corpses.Count - 1; num >= 0; num--) { CorpseEntry corpseEntry = corpses[num]; if (corpseEntry == null || (Object)(object)corpseEntry.Enemy == (Object)null) { corpses.RemoveAt(num); } else { NetworkObject networkObject = ((NetworkBehaviour)corpseEntry.Enemy).NetworkObject; if ((Object)(object)networkObject == (Object)null || !networkObject.IsSpawned) { corpses.RemoveAt(num); } } } } private static bool ContainsEnemy(EnemyAI enemy) { foreach (CorpseEntry corpse in corpses) { if (corpse != null && (Object)(object)corpse.Enemy == (Object)(object)enemy) { return true; } } return false; } private static int CountTrackedCorpses() { int num = 0; foreach (CorpseEntry corpse in corpses) { if (corpse != null && (Object)(object)corpse.Enemy != (Object)null && corpse.Enemy.isEnemyDead) { num++; } } return num; } private static bool IsServer() { NetworkManager singleton = NetworkManager.Singleton; return (Object)(object)singleton != (Object)null && singleton.IsServer; } private static string GetEnemyName(EnemyAI enemy) { if ((Object)(object)enemy == (Object)null) { return ""; } if ((Object)(object)enemy.enemyType != (Object)null && !string.IsNullOrEmpty(enemy.enemyType.enemyName)) { return enemy.enemyType.enemyName; } return ((Object)enemy).name; } } [BepInPlugin("com.brox.BM_CorpseCleaner", "BM_CorpseCleaner", "0.1.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string ModGuid = "com.brox.BM_CorpseCleaner"; public const string ModName = "BM_CorpseCleaner"; public const string ModVersion = "0.1.0"; internal static ManualLogSource Log; internal static ConfigFile PluginConfig; private static bool initialized; private void Awake() { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Expected O, but got Unknown if (!initialized) { initialized = true; Log = ((BaseUnityPlugin)this).Logger; PluginConfig = ((BaseUnityPlugin)this).Config; Log.LogInfo((object)"BM_CorpseCleaner v0.1.0 initializing..."); Configuration.Initialize(PluginConfig); CorpseManager.Initialize(); Harmony val = new Harmony("com.brox.BM_CorpseCleaner"); val.PatchAll(typeof(Plugin).Assembly); Log.LogWarning((object)"### BM_CORPSECLEANER PATCHES INSTALLED ###"); Log.LogInfo((object)("Maximum enemy corpses: " + Configuration.MaximumCorpses.Value)); Log.LogInfo((object)("Minimum corpse lifetime: " + Configuration.MinimumCorpseLifetime.Value + " seconds")); Log.LogInfo((object)"BM_CorpseCleaner loaded."); } } }