Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of UpgradeLimiter v0.4.2
UpgradeLimiter.dll
Decompiled 8 hours agousing 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"; } }