Decompiled source of NoOrbLimit v1.1.0

NoOrbLimit.dll

Decompiled 4 days ago
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[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("NoOrbLimit")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.1.0.0")]
[assembly: AssemblyInformationalVersion("1.1.0")]
[assembly: AssemblyProduct("NoOrbLimit")]
[assembly: AssemblyTitle("NoOrbLimit")]
[assembly: AssemblyVersion("1.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 NoOrbLimit
{
	[BepInPlugin("com.reponoorblimit.host", "NoOrbLimit", "1.1.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "com.reponoorblimit.host";

		public const string PluginName = "NoOrbLimit";

		public const string Version = "1.1.0";

		internal static ManualLogSource Log;

		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<bool> RemoveOrbLimit;

		internal static ConfigEntry<bool> IgnorePlayerProximity;

		internal static ConfigEntry<int> MaxOrbsPerLevel;

		internal static ConfigEntry<int> OrbDropChancePercent;

		internal static ConfigEntry<bool> VerboseLogging;

		internal static int OrbsThisLevel;

		private void Awake()
		{
			//IL_0091: Unknown result type (might be due to invalid IL or missing references)
			//IL_009b: Expected O, but got Unknown
			//IL_00c4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ce: Expected O, but got Unknown
			//IL_00fe: Unknown result type (might be due to invalid IL or missing references)
			Log = ((BaseUnityPlugin)this).Logger;
			Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Master switch for the mod.");
			RemoveOrbLimit = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "RemoveOrbLimit", true, "Ignore the vanilla per-enemy orb cap (EnemyHealth.spawnValuableMax). Enemies whose prefab sets spawnValuable = false or spawnValuableMax = 0 are left untouched.");
			IgnorePlayerProximity = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "IgnorePlayerProximity", true, "Ignore the vanilla 'player within 20 m in the last 10 seconds' rule. When true, an enemy killed anywhere drops its orb. Default: true.");
			MaxOrbsPerLevel = ((BaseUnityPlugin)this).Config.Bind<int>("General", "MaxOrbsPerLevel", 0, new ConfigDescription("Optional safety cap for enemy orbs spawned per level. 0 = no cap (recommended).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 1000), Array.Empty<object>()));
			OrbDropChancePercent = ((BaseUnityPlugin)this).Config.Bind<int>("General", "OrbDropChancePercent", 100, new ConfigDescription("Extra drop chance applied once per death. 100 = always drop (required for the 'exactly one orb per death' guarantee).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>()));
			VerboseLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "VerboseLogging", false, "Log every despawn decision: enemy name, spawnValuable, spawnValuableMax, spawnValuableCurrent, healthCurrent, valuableSpawnTimer, difficulty and the final reason.");
			OrbsThisLevel = 0;
			try
			{
				new Harmony("com.reponoorblimit.host").PatchAll(typeof(Plugin).Assembly);
				Log.LogInfo((object)"[NoOrbLimit] v1.1.0 loaded.");
				Log.LogInfo((object)($"[NoOrbLimit] Settings: Enabled={Enabled.Value}, RemoveOrbLimit={RemoveOrbLimit.Value}, " + $"IgnorePlayerProximity={IgnorePlayerProximity.Value}, MaxOrbsPerLevel={MaxOrbsPerLevel.Value}, " + $"OrbDropChancePercent={OrbDropChancePercent.Value}, VerboseLogging={VerboseLogging.Value}"));
				Log.LogInfo((object)"[NoOrbLimit] Patched EnemyParent.Despawn, EnemyHealth.OnSpawn and LevelGenerator.Start. Orb drops are decided by the host (master client); clients do not need this mod.");
			}
			catch (Exception ex)
			{
				Log.LogError((object)("[NoOrbLimit] Harmony patching failed: " + ex));
			}
		}
	}
}
namespace NoOrbLimit.Patches
{
	[HarmonyPatch(typeof(EnemyHealth), "OnSpawn")]
	internal static class EnemyHealthOnSpawnPatch
	{
		[HarmonyPostfix]
		private static void Postfix(EnemyHealth __instance)
		{
			OrbDropState.Reset(__instance);
		}
	}
	[HarmonyPatch(typeof(EnemyParent), "Despawn")]
	internal static class EnemyParentDespawnPatch
	{
		private sealed class PatchState
		{
			public EnemyHealth Health;

			public bool OriginalSpawnValuable;

			public int OriginalSpawnValuableMax;

			public int OriginalSpawnValuableCurrent;

			public bool SpawnValuableMaxModified;

			public bool SpawnValuableModified;

			public bool Eligible;

			public bool Blocked;

			public string BlockReason;

			public bool ProximityForced;
		}

		private static readonly FieldInfo SpawnValuableCurrentField = AccessTools.Field(typeof(EnemyHealth), "spawnValuableCurrent");

		private static readonly FieldInfo HealthCurrentField = AccessTools.Field(typeof(EnemyHealth), "healthCurrent");

		private static readonly FieldInfo ValuableSpawnTimerField = AccessTools.Field(typeof(EnemyParent), "valuableSpawnTimer");

		private static bool _reflectionWarningLogged;

		[HarmonyPrefix]
		private static void Prefix(EnemyParent __instance, out PatchState __state)
		{
			__state = null;
			if (!Plugin.Enabled.Value || (Object)(object)__instance == (Object)null || !SemiFunc.IsMasterClientOrSingleplayer())
			{
				return;
			}
			if (SpawnValuableCurrentField == null || HealthCurrentField == null || ValuableSpawnTimerField == null)
			{
				if (!_reflectionWarningLogged)
				{
					_reflectionWarningLogged = true;
					Plugin.Log.LogError((object)"[NoOrbLimit] Required EnemyHealth/EnemyParent fields were not found via reflection. The mod stays inactive; vanilla behavior is kept.");
				}
				return;
			}
			EnemyHealth componentInChildren = ((Component)__instance).GetComponentInChildren<EnemyHealth>(true);
			if ((Object)(object)componentInChildren == (Object)null)
			{
				if (Plugin.VerboseLogging.Value)
				{
					Plugin.Log.LogInfo((object)("[NoOrbLimit] Despawn: enemy='" + __instance.enemyName + "' has no EnemyHealth component -> this enemy can never drop an orb (vanilla rule Enemy.HasHealth == false)."));
				}
				return;
			}
			int num = (int)HealthCurrentField.GetValue(componentInChildren);
			float num2 = (float)ValuableSpawnTimerField.GetValue(__instance);
			if (num > 0)
			{
				OrbDropState.Reset(componentInChildren);
				if (Plugin.VerboseLogging.Value)
				{
					Plugin.Log.LogInfo((object)($"[NoOrbLimit] Despawn: enemy='{__instance.enemyName}', healthCurrent={num} -> " + "alive, no orb (natural despawn)."));
				}
				return;
			}
			PatchState patchState = (__state = new PatchState
			{
				Health = componentInChildren,
				OriginalSpawnValuable = componentInChildren.spawnValuable,
				OriginalSpawnValuableMax = componentInChildren.spawnValuableMax,
				OriginalSpawnValuableCurrent = GetSpawnValuableCurrent(componentInChildren)
			});
			if (OrbDropState.IsOrbGiven(componentInChildren))
			{
				patchState.Blocked = true;
				patchState.BlockReason = "duplicate Despawn for the same death";
				BlockWithSpawnValuable(componentInChildren, patchState);
				LogDecision(__instance, num, num2, patchState);
				return;
			}
			bool originalSpawnValuable = patchState.OriginalSpawnValuable;
			bool flag = patchState.OriginalSpawnValuableMax <= 0;
			if (!originalSpawnValuable || flag)
			{
				patchState.Blocked = true;
				patchState.BlockReason = (flag ? "prefab spawnValuableMax = 0 (never drops)" : "prefab spawnValuable = false (never drops)");
				LogDecision(__instance, num, num2, patchState);
				return;
			}
			patchState.Eligible = true;
			if (Plugin.IgnorePlayerProximity.Value && !(num2 > 0f))
			{
				ValuableSpawnTimerField.SetValue(__instance, 10f);
				num2 = 10f;
				patchState.ProximityForced = true;
			}
			bool flag2 = patchState.OriginalSpawnValuableCurrent < patchState.OriginalSpawnValuableMax;
			if (Plugin.RemoveOrbLimit.Value)
			{
				componentInChildren.spawnValuableMax = int.MaxValue;
				patchState.SpawnValuableMaxModified = true;
				flag2 = true;
			}
			if (!flag2)
			{
				patchState.Blocked = true;
				patchState.BlockReason = "vanilla per-enemy cap reached (RemoveOrbLimit = false)";
				BlockWithSpawnValuable(componentInChildren, patchState);
				LogDecision(__instance, num, num2, patchState);
				return;
			}
			if (!(num2 > 0f))
			{
				patchState.Blocked = true;
				patchState.BlockReason = "proximity timer is 0 (IgnorePlayerProximity = false)";
				BlockWithSpawnValuable(componentInChildren, patchState);
				LogDecision(__instance, num, num2, patchState);
				return;
			}
			int value = Plugin.MaxOrbsPerLevel.Value;
			if (value > 0 && Plugin.OrbsThisLevel >= value)
			{
				patchState.Blocked = true;
				patchState.BlockReason = $"level cap reached ({Plugin.OrbsThisLevel}/{value})";
			}
			else
			{
				int value2 = Plugin.OrbDropChancePercent.Value;
				if (value2 < 100 && Random.Range(0, 100) >= value2)
				{
					patchState.Blocked = true;
					patchState.BlockReason = $"drop chance failed ({value2}%)";
				}
			}
			if (patchState.Blocked)
			{
				BlockWithSpawnValuable(componentInChildren, patchState);
			}
			LogDecision(__instance, num, num2, patchState);
		}

		[HarmonyPostfix]
		private static void Postfix(EnemyParent __instance, PatchState __state)
		{
			if (__state == null || (Object)(object)__state.Health == (Object)null)
			{
				return;
			}
			int num = GetSpawnValuableCurrent(__state.Health) - __state.OriginalSpawnValuableCurrent;
			if (num > 0)
			{
				Plugin.OrbsThisLevel += num;
				OrbDropState.MarkOrbGiven(__state.Health);
				if (Plugin.VerboseLogging.Value)
				{
					Plugin.Log.LogInfo((object)($"[NoOrbLimit] Orb spawned: enemy='{__instance.enemyName}', +{num}, " + $"orbs this level={Plugin.OrbsThisLevel}"));
				}
			}
			else if (__state.Blocked)
			{
				OrbDropState.MarkOrbGiven(__state.Health);
			}
			else if (__state.Eligible && Plugin.VerboseLogging.Value)
			{
				Plugin.Log.LogWarning((object)("[NoOrbLimit] Despawn finished without spawning an orb, but no NoOrbLimit gate blocked it. enemy='" + __instance.enemyName + "'. Check the log for an exception or another mod."));
			}
		}

		[HarmonyFinalizer]
		private static Exception Finalizer(EnemyParent __instance, PatchState __state, Exception __exception)
		{
			RestoreOriginalFields(__state);
			if (__exception != null)
			{
				Plugin.Log.LogWarning((object)("[NoOrbLimit] EnemyParent.Despawn threw an exception; original field values were restored. " + __exception.GetType().Name + ": " + __exception.Message));
			}
			return __exception;
		}

		private static void BlockWithSpawnValuable(EnemyHealth health, PatchState state)
		{
			health.spawnValuable = false;
			state.SpawnValuableModified = true;
		}

		private static void LogDecision(EnemyParent enemy, int healthCurrent, float valuableSpawnTimer, PatchState state)
		{
			//IL_004b: Unknown result type (might be due to invalid IL or missing references)
			if (Plugin.VerboseLogging.Value)
			{
				string arg = (state.Blocked ? ("result=blocked (" + state.BlockReason + ")") : "result=allowed");
				Plugin.Log.LogInfo((object)($"[NoOrbLimit] Despawn: enemy='{enemy.enemyName}', difficulty={enemy.difficulty}, " + $"spawnValuable={state.OriginalSpawnValuable}, spawnValuableMax={state.OriginalSpawnValuableMax}, " + $"spawnValuableCurrent={state.OriginalSpawnValuableCurrent}, healthCurrent={healthCurrent}, " + $"valuableSpawnTimer={valuableSpawnTimer:0.0}, capLifted={state.SpawnValuableMaxModified}, " + $"proximityForced={state.ProximityForced}, eligible={state.Eligible}, {arg}"));
			}
		}

		private static void RestoreOriginalFields(PatchState state)
		{
			if (state != null && !((Object)(object)state.Health == (Object)null))
			{
				if (state.SpawnValuableMaxModified)
				{
					state.Health.spawnValuableMax = state.OriginalSpawnValuableMax;
					state.SpawnValuableMaxModified = false;
				}
				if (state.SpawnValuableModified)
				{
					state.Health.spawnValuable = state.OriginalSpawnValuable;
					state.SpawnValuableModified = false;
				}
			}
		}

		private static int GetSpawnValuableCurrent(EnemyHealth health)
		{
			object value = SpawnValuableCurrentField.GetValue(health);
			if (value is int)
			{
				return (int)value;
			}
			return 0;
		}
	}
	[HarmonyPatch(typeof(LevelGenerator), "Start")]
	internal static class LevelGeneratorStartPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			int orbsThisLevel = Plugin.OrbsThisLevel;
			Plugin.OrbsThisLevel = 0;
			if (Plugin.VerboseLogging.Value)
			{
				Plugin.Log.LogInfo((object)$"[NoOrbLimit] New level detected: orb counter reset ({orbsThisLevel} -> 0).");
			}
		}
	}
	internal static class OrbDropState
	{
		private sealed class State
		{
			public bool OrbGiven;
		}

		private static readonly ConditionalWeakTable<EnemyHealth, State> States = new ConditionalWeakTable<EnemyHealth, State>();

		private static State Get(EnemyHealth health)
		{
			return States.GetValue(health, (EnemyHealth _) => new State());
		}

		public static bool IsOrbGiven(EnemyHealth health)
		{
			if ((Object)(object)health != (Object)null)
			{
				return Get(health).OrbGiven;
			}
			return false;
		}

		public static void MarkOrbGiven(EnemyHealth health)
		{
			if ((Object)(object)health != (Object)null)
			{
				Get(health).OrbGiven = true;
			}
		}

		public static void Reset(EnemyHealth health)
		{
			if ((Object)(object)health != (Object)null)
			{
				Get(health).OrbGiven = false;
			}
		}
	}
}