Decompiled source of UpgradeLimiter v0.4.2

UpgradeLimiter.dll

Decompiled 8 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using ExitGames.Client.Photon;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = "")]
[assembly: AssemblyCompany("UpgradeLimiter")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.4.2.0")]
[assembly: AssemblyInformationalVersion("0.4.2")]
[assembly: AssemblyProduct("UpgradeLimiter")]
[assembly: AssemblyTitle("UpgradeLimiter")]
[assembly: AssemblyVersion("0.4.2.0")]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
}
namespace UpgradeLimiter
{
	internal class UpgradeEntry
	{
		public string Name = "";

		public MethodInfo? Method;

		public FieldInfo? CountField;

		public ConfigEntry<bool> Enabled;

		public ConfigEntry<int> MaxStacks;

		public bool ActiveEnabled;

		public int ActiveMax;
	}
	internal static class UpgradeRegistry
	{
		internal static readonly List<UpgradeEntry> Entries = new List<UpgradeEntry>();

		internal static readonly Dictionary<MethodBase, UpgradeEntry> ByMethod = new Dictionary<MethodBase, UpgradeEntry>();

		internal static readonly Dictionary<string, UpgradeEntry> ByDictName = new Dictionary<string, UpgradeEntry>(StringComparer.Ordinal);

		private static readonly (string Name, string MethodName, string DictField)[] BaseMap = new(string, string, string)[13]
		{
			("Health", "UpgradePlayerHealth", "playerUpgradeHealth"),
			("Energy", "UpgradePlayerEnergy", "playerUpgradeStamina"),
			("ExtraJump", "UpgradePlayerExtraJump", "playerUpgradeExtraJump"),
			("TumbleLaunch", "UpgradePlayerTumbleLaunch", "playerUpgradeLaunch"),
			("TumbleClimb", "UpgradePlayerTumbleClimb", "playerUpgradeTumbleClimb"),
			("TumbleWings", "UpgradePlayerTumbleWings", "playerUpgradeTumbleWings"),
			("SprintSpeed", "UpgradePlayerSprintSpeed", "playerUpgradeSpeed"),
			("CrouchRest", "UpgradePlayerCrouchRest", "playerUpgradeCrouchRest"),
			("GrabStrength", "UpgradePlayerGrabStrength", "playerUpgradeStrength"),
			("ThrowStrength", "UpgradePlayerThrowStrength", "playerUpgradeThrow"),
			("GrabRange", "UpgradePlayerGrabRange", "playerUpgradeRange"),
			("DeathHeadBattery", "UpgradeDeathHeadBattery", "playerUpgradeDeathHeadBattery"),
			("MapPlayerCount", "UpgradeMapPlayerCount", "playerUpgradeMapPlayerCount")
		};

		public static void Discover()
		{
			Entries.Clear();
			ByMethod.Clear();
			ByDictName.Clear();
			Type type = AccessTools.TypeByName("PunManager");
			Type type2 = AccessTools.TypeByName("StatsManager");
			if (type == null)
			{
				Plugin.Log.LogError((object)"[Discover] PunManager not found — base entries unenforceable.");
			}
			if (type2 == null)
			{
				Plugin.Log.LogError((object)"[Discover] StatsManager not found — base entries unenforceable.");
			}
			HashSet<MethodInfo> hashSet = new HashSet<MethodInfo>();
			(string, string, string)[] baseMap = BaseMap;
			for (int i = 0; i < baseMap.Length; i++)
			{
				(string, string, string) tuple = baseMap[i];
				string item = tuple.Item1;
				string item2 = tuple.Item2;
				string item3 = tuple.Item3;
				MethodInfo methodInfo = null;
				FieldInfo fieldInfo = null;
				if (type != null)
				{
					methodInfo = AccessTools.Method(type, item2, new Type[2]
					{
						typeof(string),
						typeof(int)
					}, (Type[])null);
				}
				if (type2 != null)
				{
					fieldInfo = AccessTools.Field(type2, item3);
				}
				if (methodInfo == null)
				{
					Plugin.Log.LogWarning((object)("[Discover] " + item2 + " not on PunManager — " + item + " cap won't enforce."));
				}
				if (fieldInfo == null)
				{
					Plugin.Log.LogWarning((object)("[Discover] " + item3 + " not on StatsManager — " + item + " cap won't enforce."));
				}
				UpgradeEntry upgradeEntry = new UpgradeEntry
				{
					Name = item,
					Method = methodInfo,
					CountField = fieldInfo
				};
				if (methodInfo != null && fieldInfo != null)
				{
					ByMethod[methodInfo] = upgradeEntry;
					hashSet.Add(methodInfo);
					Plugin.Log.LogInfo((object)("[Discover] " + item2 + " ↔ " + item3));
				}
				if (fieldInfo != null)
				{
					ByDictName[fieldInfo.Name] = upgradeEntry;
				}
				Entries.Add(upgradeEntry);
			}
			if (type2 != null)
			{
				ScanModded(type2, hashSet);
			}
			Plugin.Log.LogInfo((object)$"[Discover] {Entries.Count} entries total ({ByMethod.Count} enforceable).");
		}

		private static void ScanModded(Type statsManager, HashSet<MethodInfo> seen)
		{
			Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
			foreach (Assembly assembly in assemblies)
			{
				Type[] array;
				try
				{
					array = assembly.GetTypes();
				}
				catch (ReflectionTypeLoadException ex)
				{
					array = ex.Types ?? Array.Empty<Type>();
				}
				catch
				{
					continue;
				}
				Type[] array2 = array;
				foreach (Type type in array2)
				{
					if (type == null)
					{
						continue;
					}
					MethodInfo[] methods;
					try
					{
						methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
					}
					catch
					{
						continue;
					}
					MethodInfo[] array3 = methods;
					foreach (MethodInfo methodInfo in array3)
					{
						if (seen.Contains(methodInfo) || !methodInfo.Name.StartsWith("Upgrade", StringComparison.Ordinal) || methodInfo.ReturnType != typeof(int))
						{
							continue;
						}
						ParameterInfo[] parameters = methodInfo.GetParameters();
						if (parameters.Length != 2 || parameters[0].ParameterType != typeof(string) || parameters[1].ParameterType != typeof(int))
						{
							continue;
						}
						FieldInfo fieldInfo = FindDictField(methodInfo, statsManager);
						if (!(fieldInfo == null))
						{
							string text = (methodInfo.Name.StartsWith("UpgradePlayer", StringComparison.Ordinal) ? methodInfo.Name.Substring("UpgradePlayer".Length) : methodInfo.Name.Substring("Upgrade".Length));
							string name = text;
							if (Entries.Exists((UpgradeEntry e) => e.Name == name))
							{
								name = type.Name + "_" + name;
							}
							UpgradeEntry upgradeEntry = new UpgradeEntry
							{
								Name = name,
								Method = methodInfo,
								CountField = fieldInfo
							};
							ByMethod[methodInfo] = upgradeEntry;
							ByDictName[fieldInfo.Name] = upgradeEntry;
							seen.Add(methodInfo);
							Entries.Add(upgradeEntry);
							Plugin.Log.LogInfo((object)("[Discover] Modded " + type.FullName + "." + methodInfo.Name + " ↔ " + fieldInfo.Name + " as " + name));
						}
					}
				}
			}
		}

		private static FieldInfo? FindDictField(MethodInfo method, Type statsManager)
		{
			MethodBody methodBody;
			try
			{
				methodBody = method.GetMethodBody();
			}
			catch
			{
				return null;
			}
			if (methodBody == null)
			{
				return null;
			}
			byte[] iLAsByteArray = methodBody.GetILAsByteArray();
			if (iLAsByteArray == null || iLAsByteArray.Length < 5)
			{
				return null;
			}
			Module module = method.Module;
			Type typeFromHandle = typeof(Dictionary<string, int>);
			Type[] genericTypeArguments = null;
			Type[] genericMethodArguments = null;
			try
			{
				Type? declaringType = method.DeclaringType;
				if ((object)declaringType != null && declaringType.IsGenericType)
				{
					genericTypeArguments = method.DeclaringType.GetGenericArguments();
				}
				if (method.IsGenericMethod)
				{
					genericMethodArguments = method.GetGenericArguments();
				}
			}
			catch
			{
			}
			for (int i = 0; i <= iLAsByteArray.Length - 5; i++)
			{
				byte b = iLAsByteArray[i];
				if (b == 123 || b == 124 || b == 125 || b == 126 || b == 128)
				{
					int metadataToken = BitConverter.ToInt32(iLAsByteArray, i + 1);
					FieldInfo fieldInfo;
					try
					{
						fieldInfo = module.ResolveField(metadataToken, genericTypeArguments, genericMethodArguments);
					}
					catch
					{
						continue;
					}
					if (!(fieldInfo == null) && !(fieldInfo.DeclaringType != statsManager) && !(fieldInfo.FieldType != typeFromHandle))
					{
						return fieldInfo;
					}
				}
			}
			return null;
		}
	}
	[BepInPlugin("darkharasho.UpgradeLimiter", "UpgradeLimiter", "0.4.2")]
	public class Plugin : BaseUnityPlugin
	{
		internal static ManualLogSource Log;

		internal static ConfigEntry<bool> SyncToClients;

		private void Awake()
		{
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Expected O, but got Unknown
			//IL_0095: Unknown result type (might be due to invalid IL or missing references)
			//IL_009b: Expected O, but got Unknown
			//IL_01e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e7: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			SyncToClients = ((BaseUnityPlugin)this).Config.Bind<bool>("Sync", "SyncToClients", true, "Host-only. When true, the host pushes its limits to every client via Photon room properties. When false, the host never publishes; each client uses its own local config.");
			UpgradeRegistry.Discover();
			BindUpgradeConfigs();
			ResetActiveToLocal();
			((Component)this).gameObject.AddComponent<SettingsSyncer>();
			SyncToClients.SettingChanged += delegate
			{
				SettingsSyncer.Instance?.PushHostSettingsExternal();
			};
			Harmony val = new Harmony("darkharasho.UpgradeLimiter");
			val.PatchAll();
			HarmonyMethod val2 = new HarmonyMethod(typeof(CapPrefix).GetMethod("Prefix"));
			foreach (UpgradeEntry entry in UpgradeRegistry.Entries)
			{
				if (!(entry.Method == null))
				{
					try
					{
						val.Patch((MethodBase)entry.Method, val2, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
						Log.LogInfo((object)("[Patch] Installed cap prefix on " + entry.Method.Name));
					}
					catch (Exception ex)
					{
						Log.LogError((object)("[Patch] Failed to patch " + entry.Method.Name + ": " + ex.GetType().Name + " " + ex.Message));
					}
				}
			}
			Type type = AccessTools.TypeByName("StatsManager");
			MethodInfo methodInfo = ((type != null) ? AccessTools.Method(type, "DictionaryUpdateValue", new Type[3]
			{
				typeof(string),
				typeof(string),
				typeof(int)
			}, (Type[])null) : null);
			if (methodInfo != null)
			{
				try
				{
					HarmonyMethod val3 = new HarmonyMethod(typeof(DictUpdateClampPrefix).GetMethod("Prefix"));
					val.Patch((MethodBase)methodInfo, val3, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
					Log.LogInfo((object)"[Patch] Installed clamp prefix on StatsManager.DictionaryUpdateValue");
				}
				catch (Exception ex2)
				{
					Log.LogError((object)("[Patch] Failed to patch DictionaryUpdateValue: " + ex2.GetType().Name + " " + ex2.Message));
				}
			}
			else
			{
				Log.LogWarning((object)"[Patch] StatsManager.DictionaryUpdateValue not found — shared/RPC clamp disabled.");
			}
			Log.LogInfo((object)"UpgradeLimiter v0.4.2 loaded.");
		}

		private void BindUpgradeConfigs()
		{
			//IL_0087: Unknown result type (might be due to invalid IL or missing references)
			//IL_0091: Expected O, but got Unknown
			AcceptableValueRange<int> val = new AcceptableValueRange<int>(0, 99);
			foreach (UpgradeEntry entry2 in UpgradeRegistry.Entries)
			{
				string name = entry2.Name;
				entry2.Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>(name, "Enabled", false, "Enable the cap for the " + entry2.Name + " upgrade. When false, the upgrade behaves vanilla.");
				entry2.MaxStacks = ((BaseUnityPlugin)this).Config.Bind<int>(name, "MaxStacks", 5, new ConfigDescription("Maximum number of " + entry2.Name + " upgrades a single player may stack. 0 means no further upgrades can be picked up.", (AcceptableValueBase)(object)val, Array.Empty<object>()));
				UpgradeEntry entry = entry2;
				entry.Enabled.SettingChanged += delegate
				{
					if (!PhotonNetwork.InRoom || PhotonNetwork.IsMasterClient)
					{
						entry.ActiveEnabled = entry.Enabled.Value;
						if (PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient)
						{
							SettingsSyncer.Instance?.PushHostSettingsExternal();
						}
					}
				};
				entry.MaxStacks.SettingChanged += delegate
				{
					if (!PhotonNetwork.InRoom || PhotonNetwork.IsMasterClient)
					{
						entry.ActiveMax = entry.MaxStacks.Value;
						if (PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient)
						{
							SettingsSyncer.Instance?.PushHostSettingsExternal();
						}
					}
				};
			}
		}

		internal static void ResetActiveToLocal()
		{
			foreach (UpgradeEntry entry in UpgradeRegistry.Entries)
			{
				entry.ActiveEnabled = entry.Enabled.Value;
				entry.ActiveMax = entry.MaxStacks.Value;
			}
		}
	}
	internal class SettingsSyncer : MonoBehaviour
	{
		internal static SettingsSyncer? Instance;

		private bool _wasInRoom;

		private bool _wasMaster;

		private float _pollDelay;

		private readonly Dictionary<string, (bool en, int max)> _lastPushed = new Dictionary<string, (bool, int)>();

		private void Awake()
		{
			Instance = this;
		}

		private void Start()
		{
			Plugin.Log.LogInfo((object)"[Sync] SettingsSyncer ready (polling mode)");
		}

		private void Update()
		{
			bool inRoom = PhotonNetwork.InRoom;
			bool flag = inRoom && PhotonNetwork.IsMasterClient;
			if (inRoom && !_wasInRoom)
			{
				if (flag)
				{
					PushHostSettings();
				}
				else
				{
					PullHostSettings();
				}
			}
			else if (!inRoom && _wasInRoom)
			{
				Plugin.ResetActiveToLocal();
				Plugin.Log.LogInfo((object)"[Sync] Left room — reset to local config");
			}
			else if (inRoom && flag && !_wasMaster)
			{
				PushHostSettings();
			}
			else if (inRoom && !flag)
			{
				_pollDelay -= Time.unscaledDeltaTime;
				if (_pollDelay <= 0f)
				{
					_pollDelay = 1f;
					PullHostSettings();
				}
			}
			_wasInRoom = inRoom;
			_wasMaster = flag;
		}

		internal void PushHostSettingsExternal()
		{
			if (PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient)
			{
				PushHostSettings();
			}
		}

		private void PushHostSettings()
		{
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_001b: Expected O, but got Unknown
			if (PhotonNetwork.CurrentRoom == null || !Plugin.SyncToClients.Value)
			{
				return;
			}
			Hashtable val = new Hashtable();
			bool flag = false;
			foreach (UpgradeEntry entry in UpgradeRegistry.Entries)
			{
				bool value = entry.Enabled.Value;
				int value2 = entry.MaxStacks.Value;
				if (!_lastPushed.TryGetValue(entry.Name, out (bool, int) value3) || value3.Item1 != value || value3.Item2 != value2)
				{
					_lastPushed[entry.Name] = (value, value2);
					val[(object)("UL_" + entry.Name + "_E")] = value;
					val[(object)("UL_" + entry.Name + "_M")] = value2;
					flag = true;
				}
			}
			if (flag)
			{
				PhotonNetwork.CurrentRoom.SetCustomProperties(val, (Hashtable)null, (WebFlags)null);
				Plugin.Log.LogInfo((object)$"[Sync] Host pushed {((Dictionary<object, object>)(object)val).Count / 2} upgrade limit settings");
			}
		}

		private void PullHostSettings()
		{
			Room currentRoom = PhotonNetwork.CurrentRoom;
			Hashtable val = ((currentRoom != null) ? ((RoomInfo)currentRoom).CustomProperties : null);
			if (val == null)
			{
				return;
			}
			bool flag = false;
			foreach (UpgradeEntry entry in UpgradeRegistry.Entries)
			{
				string text = "UL_" + entry.Name + "_E";
				string text2 = "UL_" + entry.Name + "_M";
				if (((Dictionary<object, object>)(object)val).ContainsKey((object)text) && val[(object)text] is bool activeEnabled)
				{
					entry.ActiveEnabled = activeEnabled;
					flag = true;
				}
				if (((Dictionary<object, object>)(object)val).ContainsKey((object)text2) && val[(object)text2] is int activeMax)
				{
					entry.ActiveMax = activeMax;
					flag = true;
				}
			}
			if (flag)
			{
				Plugin.Log.LogInfo((object)"[Sync] Pulled host upgrade-limit settings from room properties");
			}
		}
	}
	internal static class DictUpdateClampPrefix
	{
		public static void Prefix(string dictionaryName, string key, ref int value)
		{
			if (UpgradeRegistry.ByDictName.TryGetValue(dictionaryName, out UpgradeEntry value2) && value2.ActiveEnabled && value > value2.ActiveMax)
			{
				Plugin.Log.LogDebug((object)$"[Cap] {value2.Name} for {key} clamped {value} → {value2.ActiveMax} (DictionaryUpdateValue)");
				value = value2.ActiveMax;
			}
		}
	}
	internal static class CapPrefix
	{
		public static bool Prefix(string _steamID, int value, MethodBase __originalMethod)
		{
			if (!UpgradeRegistry.ByMethod.TryGetValue(__originalMethod, out UpgradeEntry value2))
			{
				return true;
			}
			if (!value2.ActiveEnabled)
			{
				return true;
			}
			if (value2.CountField == null)
			{
				return true;
			}
			if (value <= 0)
			{
				return true;
			}
			Type type = AccessTools.TypeByName("StatsManager");
			object obj = ((type != null) ? AccessTools.Field(type, "instance") : null)?.GetValue(null);
			if (obj == null)
			{
				return true;
			}
			if (!(value2.CountField.GetValue(obj) is IDictionary<string, int> dictionary))
			{
				return true;
			}
			if (!dictionary.TryGetValue(_steamID, out var value3))
			{
				value3 = 0;
			}
			if (value3 + value > value2.ActiveMax)
			{
				Plugin.Log.LogDebug((object)$"[Cap] {value2.Name} for {_steamID} blocked: {value3}+{value} > {value2.ActiveMax}");
				return false;
			}
			return true;
		}
	}
	public static class PluginInfo
	{
		public const string PLUGIN_GUID = "darkharasho.UpgradeLimiter";

		public const string PLUGIN_NAME = "UpgradeLimiter";

		public const string PLUGIN_VERSION = "0.4.2";
	}
}