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 Enabled; public ConfigEntry MaxStacks; public bool ActiveEnabled; public int ActiveMax; } internal static class UpgradeRegistry { internal static readonly List Entries = new List(); internal static readonly Dictionary ByMethod = new Dictionary(); internal static readonly Dictionary ByDictName = new Dictionary(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 hashSet = new HashSet(); (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 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(); } 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); 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 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("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(); 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 val = new AcceptableValueRange(0, 99); foreach (UpgradeEntry entry2 in UpgradeRegistry.Entries) { string name = entry2.Name; entry2.Enabled = ((BaseUnityPlugin)this).Config.Bind(name, "Enabled", false, "Enable the cap for the " + entry2.Name + " upgrade. When false, the upgrade behaves vanilla."); entry2.MaxStacks = ((BaseUnityPlugin)this).Config.Bind(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())); 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 _lastPushed = new Dictionary(); 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)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)val).ContainsKey((object)text) && val[(object)text] is bool activeEnabled) { entry.ActiveEnabled = activeEnabled; flag = true; } if (((Dictionary)(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 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"; } }