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";
}
}