Decompiled source of BM CorpseCleaner v0.1.0

BM_CorpseCleaner.dll

Decompiled 7 hours ago
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<int> MaximumCorpses;

	internal static ConfigEntry<float> MinimumCorpseLifetime;

	internal static void Initialize(ConfigFile config)
	{
		MaximumCorpses = config.Bind<int>("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<float>("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<int> maximumCorpses = MaximumCorpses;
		IntSliderOptions val = new IntSliderOptions();
		((BaseRangeOptions<int>)val).Min = 1;
		((BaseRangeOptions<int>)val).Max = 100;
		((BaseOptions)val).RequiresRestart = false;
		LethalConfigManager.AddConfigItem((BaseConfigItem)new IntSliderConfigItem(maximumCorpses, val));
		ConfigEntry<float> minimumCorpseLifetime = MinimumCorpseLifetime;
		FloatSliderOptions val2 = new FloatSliderOptions();
		((BaseRangeOptions<float>)val2).Min = 0f;
		((BaseRangeOptions<float>)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<CorpseEntry> corpses = new List<CorpseEntry>();

	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<EnemyAI> 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 "<null>";
		}
		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.");
		}
	}
}