using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using BetterTeamUpgrades.Config; using BetterTeamUpgrades.Patches; using HarmonyLib; using Photon.Pun; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("BetterTeamUpgrades")] [assembly: AssemblyDescription("DLL for REPO mod.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("MrBytesized")] [assembly: AssemblyProduct("BetterTeamUpgrades")] [assembly: AssemblyCopyright("Copyright © 2025")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("ef74d5e5-8fe6-4b6a-86ed-0e29e12695bb")] [assembly: AssemblyFileVersion("2.2.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("2.2.0.0")] namespace BetterTeamUpgrades { [BepInPlugin("MrBytesized.REPO.BetterTeamUpgrades", "Better Team Upgrades", "2.2.1")] public class Plugin : BaseUnityPlugin { private const string mod_guid = "MrBytesized.REPO.BetterTeamUpgrades"; private const string mod_name = "Better Team Upgrades"; private const string mod_version = "2.2.1"; private readonly Harmony harmony = new Harmony("MrBytesized.REPO.BetterTeamUpgrades"); private static Plugin instance; internal static ManualLogSource Log; private (ConfigEntry configEntry, Action enablePatch, Action disablePatch, string description)[] _patchArray; public static ConfigFile PlguinConfig; internal static readonly object RandomLock = new object(); internal static readonly Random Random = new Random(); private void Awake() { if ((Object)(object)instance == (Object)null) { instance = this; } Log = Logger.CreateLogSource("MrBytesized.REPO.BetterTeamUpgrades"); PlguinConfig = ((BaseUnityPlugin)this).Config; harmony.PatchAll(typeof(StatsManagerInitPatch)); Configuration.Init(PlguinConfig); _patchArray = new(ConfigEntry, Action, Action, string)[2] { (Configuration.EnableSharedUpgradesPatch, delegate { harmony.PatchAll(typeof(SharedUpgradesPatch)); }, delegate { harmony.UnpatchSelf(typeof(SharedUpgradesPatch)); }, "Shared Upgrades"), (Configuration.EnableLateJoinPlayerUpdateSyncPatch, delegate { harmony.PatchAll(typeof(LateJoinPlayerUpgradeSyncPatch)); }, delegate { harmony.UnpatchSelf(typeof(LateJoinPlayerUpgradeSyncPatch)); }, "Late Join Player Upgrade Sync") }; (ConfigEntry, Action, Action, string)[] patchArray = _patchArray; for (int i = 0; i < patchArray.Length; i++) { var (configEntry, enablePatch, disablePatch, description) = patchArray[i]; UpdatePatchFromConfig(configEntry, enablePatch, disablePatch, description); configEntry.SettingChanged += delegate { UpdatePatchFromConfig(configEntry, enablePatch, disablePatch, description); }; } Log.LogInfo((object)"Better Team Upgrades mod has been activated"); } private void UpdatePatchFromConfig(ConfigEntry configEntry, Action enablePatch, Action disablePatch, string description) { if (configEntry.Value) { try { enablePatch(); Log.LogInfo((object)(description + " patch enabled.")); return; } catch (Exception ex) { Log.LogError((object)("Failed to enable " + description + ": " + ex.Message)); return; } } try { disablePatch(); Log.LogInfo((object)(description + " patch disabled.")); } catch (Exception ex2) { Log.LogError((object)("Failed to disable " + description + ": " + ex2.Message)); } } internal static int Roll(int min, int max) { lock (RandomLock) { return Random.Next(min, max); } } public static IEnumerable>> GetDictionaryOfDictionaries(StatsManager instance) { if ((Object)(object)instance == (Object)null) { return Enumerable.Empty>>(); } FieldInfo fieldInfo = AccessTools.Field(typeof(StatsManager), "dictionaryOfDictionaries"); if (fieldInfo == null) { Log.LogError((object)"StatsManager.dictionaryOfDictionaries field not found via reflection!"); return Enumerable.Empty>>(); } object value = fieldInfo.GetValue(instance); if (value == null) { return Enumerable.Empty>>(); } return (IEnumerable>>)value; } } public static class HarmonyExtensions { public static void UnpatchSelf(this Harmony harmony, Type patchClass) { HarmonyPatch[] array = patchClass.GetCustomAttributes(typeof(HarmonyPatch), inherit: true).OfType().ToArray(); for (int i = 0; i < array.Length; i++) { HarmonyMethod info = ((HarmonyAttribute)array[i]).info; if (info == null) { Plugin.Log.LogWarning((object)("Invalid HarmonyPatch method info on class: " + patchClass.FullName)); continue; } MethodInfo methodInfo = ResolveOriginal(info); if (methodInfo == null) { Plugin.Log.LogWarning((object)("Original method not found for class patch: " + FormatInfo(info))); } else { harmony.Unpatch((MethodBase)methodInfo, (HarmonyPatchType)0, harmony.Id); } } MethodInfo[] methods = patchClass.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo2 in methods) { HarmonyPatch[] array2 = methodInfo2.GetCustomAttributes(typeof(HarmonyPatch), inherit: true).OfType().ToArray(); if (array2.Length == 0) { continue; } array = array2; for (int j = 0; j < array.Length; j++) { HarmonyMethod info2 = ((HarmonyAttribute)array[j]).info; if (info2 == null) { Plugin.Log.LogWarning((object)("Invalid HarmonyPatch info on method: " + methodInfo2.DeclaringType.FullName + "." + methodInfo2.Name)); continue; } MethodInfo methodInfo3 = ResolveOriginal(info2); if (methodInfo3 == null) { Plugin.Log.LogWarning((object)("Original method not found for method-level patch: " + FormatInfo(info2))); } else { harmony.Unpatch((MethodBase)methodInfo3, (HarmonyPatchType)0, harmony.Id); } } } } private static MethodInfo ResolveOriginal(HarmonyMethod info) { if (info.method != null) { return info.method; } if (info.declaringType == null || string.IsNullOrEmpty(info.methodName)) { return null; } return AccessTools.Method(info.declaringType, info.methodName, info.argumentTypes, (Type[])null); } private static string FormatInfo(HarmonyMethod info) { string obj = ((info.declaringType != null) ? info.declaringType.FullName : ""); string text = ((!string.IsNullOrEmpty(info.methodName)) ? info.methodName : ""); return obj + "." + text; } } } namespace BetterTeamUpgrades.Patches { [HarmonyPatch(typeof(PlayerAvatar), "Start")] public class LateJoinPlayerUpgradeSyncPatch { [CompilerGenerated] private sealed class <>c__DisplayClass1_0 { public string id; public Func <>9__2; internal bool b__2(PlayerAvatar p) { return SemiFunc.PlayerGetSteamID(p) == id; } } [CompilerGenerated] private sealed class d__1 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public PlayerAvatar newPlayer; private float 5__2; private float 5__3; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__1(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Expected O, but got Unknown //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; 5__2 = 0f; 5__3 = 10f; goto IL_006c; case 1: <>1__state = -1; 5__2 += 0.5f; goto IL_006c; case 2: { <>1__state = -1; if ((Object)(object)StatsManager.instance == (Object)null || (Object)(object)PunManager.instance == (Object)null) { return false; } PhotonView component = ((Component)PunManager.instance).GetComponent(); if ((Object)(object)component == (Object)null) { Plugin.Log.LogWarning((object)"Late Join: PunManager PhotonView not found."); return false; } string text = SemiFunc.PlayerGetSteamID(newPlayer); if (string.IsNullOrEmpty(text)) { Plugin.Log.LogWarning((object)$"Late Join: Timed out waiting for SteamID for player {newPlayer.photonView.ViewID}. Skipping sync."); return false; } string text2 = (string)AccessTools.Field(typeof(PlayerAvatar), "playerName").GetValue(newPlayer); Plugin.Log.LogInfo((object)("Late Join: Player " + text2 + " (" + text + ") is ready. Starting sync...")); List source = SemiFunc.PlayerGetAll(); List list = (from p in source select SemiFunc.PlayerGetSteamID(p) into id where !string.IsNullOrEmpty(id) select id).ToList(); foreach (KeyValuePair> dictionaryOfDictionary in Plugin.GetDictionaryOfDictionaries(StatsManager.instance)) { if (!dictionaryOfDictionary.Key.StartsWith("playerUpgrade")) { continue; } string key = dictionaryOfDictionary.Key; Dictionary value = dictionaryOfDictionary.Value; bool flag = SharedUpgradesPatch.VanillaKeys.Contains(key); string text3 = (flag ? "Vanilla Upgrade Settings" : "Modded Upgrade Settings"); string text4 = key.Replace("player", ""); if (!Plugin.PlguinConfig.Bind(text3, text4, true, "Enable upgrade syncing for " + key).Value) { Plugin.Log.LogInfo((object)("Late Join: Skipping " + key + " because config toggle '" + text3 + ":" + text4 + "' is disabled.")); continue; } if (!flag && !Configuration.EnableCustomUpgradeSyncing.Value) { Plugin.Log.LogInfo((object)("Late Join: Custom Upgrade Syncing is disabled. Skipping: " + key)); continue; } int num = 0; foreach (string item in list) { if (value.TryGetValue(item, out var value2) && value2 > num) { num = value2; } } if (num <= 0) { continue; } using List.Enumerator enumerator2 = list.GetEnumerator(); while (enumerator2.MoveNext()) { <>c__DisplayClass1_0 CS$<>8__locals0 = new <>c__DisplayClass1_0 { id = enumerator2.Current }; int num2 = (value.ContainsKey(CS$<>8__locals0.id) ? value[CS$<>8__locals0.id] : 0); int num3 = num - num2; if (num3 <= 0) { continue; } for (int i = 0; i < num3; i++) { Plugin.Log.LogInfo((object)("Late Join: Considering sync " + key + " for " + CS$<>8__locals0.id + " (+1)")); int num4 = Plugin.Roll(0, 100); if (num4 >= Configuration.LateJoinUpgradeSyncChance.Value) { Plugin.Log.LogInfo((object)$"Late Join: Skipped syncing {key} for {CS$<>8__locals0.id} due to chance roll ({num4} >= {Configuration.LateJoinUpgradeSyncChance.Value})"); continue; } if (flag) { string text5 = key.Substring("playerUpgrade".Length); component.RPC("TesterUpgradeCommandRPC", (RpcTarget)1, new object[3] { CS$<>8__locals0.id, text5, 1 }); if (value.ContainsKey(CS$<>8__locals0.id)) { value[CS$<>8__locals0.id]++; } else { value[CS$<>8__locals0.id] = 1; } } else { component.RPC("UpdateStatRPC", (RpcTarget)1, new object[3] { key, CS$<>8__locals0.id, num2 + 1 }); value[CS$<>8__locals0.id] = num2 + 1; } string text6 = "Unknown"; PlayerAvatar val = ((IEnumerable)source).FirstOrDefault((Func)((PlayerAvatar p) => SemiFunc.PlayerGetSteamID(p) == CS$<>8__locals0.id)); if ((Object)(object)val != (Object)null) { text6 = (string)AccessTools.Field(typeof(PlayerAvatar), "playerName").GetValue(val); } Plugin.Log.LogInfo((object)("Late Join: Synced " + key + " for " + text6 + " (+1)")); num2++; } } } Plugin.Log.LogInfo((object)("Late Join: Sync complete for " + text2 + ".")); return false; } IL_006c: if (string.IsNullOrEmpty(SemiFunc.PlayerGetSteamID(newPlayer)) && 5__2 < 5__3) { <>2__current = (object)new WaitForSeconds(0.5f); <>1__state = 1; return true; } <>2__current = (object)new WaitForSeconds(1f); <>1__state = 2; return true; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [HarmonyPostfix] private static void Postfix(PlayerAvatar __instance) { if (SemiFunc.IsMasterClientOrSingleplayer() && !((Object)(object)RunManager.instance.levelCurrent == (Object)(object)RunManager.instance.levelMainMenu) && !((Object)(object)RunManager.instance.levelCurrent == (Object)(object)RunManager.instance.levelLobbyMenu) && !((Object)(object)RunManager.instance.levelCurrent == (Object)(object)RunManager.instance.levelRecording) && !((Object)(object)RunManager.instance.levelCurrent == (Object)(object)RunManager.instance.levelSplashScreen)) { ((MonoBehaviour)__instance).StartCoroutine(SyncWithDelay(__instance)); } } [IteratorStateMachine(typeof(d__1))] private static IEnumerator SyncWithDelay(PlayerAvatar newPlayer) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__1(0) { newPlayer = newPlayer }; } } [HarmonyPatch(typeof(ItemUpgrade), "PlayerUpgrade")] public class SharedUpgradesPatch { public struct UpgradeContext { public string SteamID; public int ViewID; public string PlayerName; public Dictionary PreUpgradeStats; } public static HashSet VanillaKeys = new HashSet(); public static HashSet ModdedKeys = new HashSet(); [HarmonyPrefix] public static void Prefix(ItemUpgrade __instance, out UpgradeContext __state) { __state = default(UpgradeContext); if (!SemiFunc.IsMasterClientOrSingleplayer()) { return; } object? value = AccessTools.Field(typeof(ItemUpgrade), "itemToggle").GetValue(__instance); ItemToggle val = (ItemToggle)((value is ItemToggle) ? value : null); if ((Object)(object)val == (Object)null || !val.toggleState) { return; } int num = (int)AccessTools.Field(typeof(ItemToggle), "playerTogglePhotonID").GetValue(val); PlayerAvatar val2 = SemiFunc.PlayerAvatarGetFromPhotonID(num); if ((Object)(object)val2 == (Object)null) { return; } string playerName = (string)AccessTools.Field(typeof(PlayerAvatar), "playerName").GetValue(val2); string text = (string)AccessTools.Field(typeof(PlayerAvatar), "steamID").GetValue(val2); Dictionary dictionary = new Dictionary(); if ((Object)(object)StatsManager.instance != (Object)null) { foreach (KeyValuePair> dictionaryOfDictionary in Plugin.GetDictionaryOfDictionaries(StatsManager.instance)) { if (dictionaryOfDictionary.Key.StartsWith("playerUpgrade")) { if (dictionaryOfDictionary.Value.TryGetValue(text, out var value2)) { dictionary[dictionaryOfDictionary.Key] = value2; } else { dictionary[dictionaryOfDictionary.Key] = 0; } } } } __state = new UpgradeContext { SteamID = text, ViewID = num, PlayerName = playerName, PreUpgradeStats = dictionary }; } [HarmonyPostfix] public static void Postfix(ItemUpgrade __instance, UpgradeContext __state) { if (!SemiFunc.IsMasterClientOrSingleplayer() || string.IsNullOrEmpty(__state.SteamID) || (Object)(object)PunManager.instance == (Object)null) { return; } PhotonView component = ((Component)PunManager.instance).GetComponent(); if ((Object)(object)component == (Object)null) { Plugin.Log.LogError((object)"SharedUpgrades: PunManager PhotonView not found!"); return; } foreach (KeyValuePair> dictionaryOfDictionary in Plugin.GetDictionaryOfDictionaries(StatsManager.instance)) { if (!dictionaryOfDictionary.Key.StartsWith("playerUpgrade")) { continue; } bool flag = VanillaKeys.Contains(dictionaryOfDictionary.Key); string text = (flag ? "Vanilla Upgrade Settings" : "Modded Upgrade Settings"); string text2 = dictionaryOfDictionary.Key.Replace("player", ""); if (!Plugin.PlguinConfig.Bind(text, text2, true, "Enable shared upgrade syncing for " + dictionaryOfDictionary.Key).Value) { Plugin.Log.LogInfo((object)("SharedUpgrades: Skipping " + dictionaryOfDictionary.Key + " because config toggle '" + text + ":" + text2 + "' is disabled.")); continue; } int num = (dictionaryOfDictionary.Value.ContainsKey(__state.SteamID) ? dictionaryOfDictionary.Value[__state.SteamID] : 0); int num2 = (__state.PreUpgradeStats.ContainsKey(dictionaryOfDictionary.Key) ? __state.PreUpgradeStats[dictionaryOfDictionary.Key] : 0); if (num > num2) { int num3 = num - num2; string key = dictionaryOfDictionary.Key; Plugin.Log.LogInfo((object)$"Detected upgrade: {key} (+{num3}) for {__state.PlayerName}({__state.SteamID})"); int num4 = Plugin.Roll(0, 100); if (num4 >= Configuration.SharedUpgradeChance.Value) { Plugin.Log.LogInfo((object)$"Skipped syncing {key} due to chance roll ({num4} >= {Configuration.SharedUpgradeChance.Value})"); } else if (flag) { string command = key.Substring("playerUpgrade".Length); DistributeVanillaUpgrade(component, command, num3, __state); } else if (!Configuration.EnableCustomUpgradeSyncing.Value) { Plugin.Log.LogInfo((object)("Custom Upgrade Syncing is disabled. Skipping: " + key)); } else { DistributeCustomUpgrade(component, key, num, __state); } } } } private static void DistributeVanillaUpgrade(PhotonView punView, string command, int amount, UpgradeContext context) { foreach (PlayerAvatar item in SemiFunc.PlayerGetAll()) { if ((Object)(object)item == (Object)null || (Object)(object)item.photonView == (Object)null) { continue; } if (item.photonView.ViewID == context.ViewID) { Plugin.Log.LogInfo((object)("Skipping original upgrader: " + command + " for " + context.PlayerName + "(" + context.SteamID + ")")); continue; } string text = (string)AccessTools.Field(typeof(PlayerAvatar), "playerName").GetValue(item); if (string.IsNullOrEmpty(text)) { text = "Unknown"; } string text2 = (string)AccessTools.Field(typeof(PlayerAvatar), "steamID").GetValue(item); if (!string.IsNullOrEmpty(text2)) { punView.RPC("TesterUpgradeCommandRPC", (RpcTarget)0, new object[3] { text2, command, amount }); Plugin.Log.LogInfo((object)("Synced Vanilla: " + command + " for " + text + "(" + text2 + ")")); } } } private static void DistributeCustomUpgrade(PhotonView punView, string dictionaryKey, int totalValue, UpgradeContext context) { foreach (PlayerAvatar item in SemiFunc.PlayerGetAll()) { if ((Object)(object)item == (Object)null || (Object)(object)item.photonView == (Object)null) { continue; } if (item.photonView.ViewID == context.ViewID) { Plugin.Log.LogInfo((object)("Skipping original upgrader: " + dictionaryKey + " for " + context.PlayerName + "(" + context.SteamID + ")")); continue; } string text = (string)AccessTools.Field(typeof(PlayerAvatar), "playerName").GetValue(item); if (string.IsNullOrEmpty(text)) { text = "Unknown"; } string text2 = (string)AccessTools.Field(typeof(PlayerAvatar), "steamID").GetValue(item); if (!string.IsNullOrEmpty(text2)) { punView.RPC("UpdateStatRPC", (RpcTarget)0, new object[3] { dictionaryKey, text2, totalValue }); Plugin.Log.LogInfo((object)("Synced Custom: " + dictionaryKey + " for " + text + "(" + text2 + ")")); } } } } [HarmonyPatch(typeof(StatsManager), "Start")] public class StatsManagerInitPatch { [HarmonyPostfix] public static void Postfix(StatsManager __instance) { SharedUpgradesPatch.VanillaKeys.Clear(); SharedUpgradesPatch.ModdedKeys.Clear(); HashSet hashSet = (from f in typeof(StatsManager).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) select f.Name).ToHashSet(); foreach (KeyValuePair> dictionaryOfDictionary in Plugin.GetDictionaryOfDictionaries(__instance)) { string key = dictionaryOfDictionary.Key; if (key.StartsWith("playerUpgrade")) { string text = key.Replace("player", ""); if (hashSet.Contains(key)) { SharedUpgradesPatch.VanillaKeys.Add(key); Plugin.PlguinConfig.Bind("Vanilla Upgrade Settings", text, true, "Enable shared upgrade syncing for " + key); } else { SharedUpgradesPatch.ModdedKeys.Add(key); Plugin.PlguinConfig.Bind("Modded Upgrade Settings", text, true, "Enable shared upgrade syncing for modded upgrade " + key); } } } Plugin.Log.LogInfo((object)$"Auto-discovered {SharedUpgradesPatch.VanillaKeys.Count} vanilla upgrade keys and {SharedUpgradesPatch.ModdedKeys.Count} modded upgrade keys."); } } } namespace BetterTeamUpgrades.Config { internal class Configuration { public static ConfigEntry EnableSharedUpgradesPatch; public static ConfigEntry SharedUpgradeChance; public static ConfigEntry EnableLateJoinPlayerUpdateSyncPatch; public static ConfigEntry LateJoinUpgradeSyncChance; public static ConfigEntry EnableCustomUpgradeSyncing; public static void Init(ConfigFile config) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Expected O, but got Unknown //IL_0049: Expected O, but got Unknown //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Expected O, but got Unknown //IL_0097: Expected O, but got Unknown EnableSharedUpgradesPatch = config.Bind("Shared Upgrade Settings", "EnableSharedUpgrades", true, "Enables Shared Upgrades for all supported Upgrades"); SharedUpgradeChance = config.Bind(new ConfigDefinition("Shared Upgrade Settings", "SharedUpgradeChance"), 100, new ConfigDescription("The percentage chance (0-100) that an upgrade will be shared with team members when purchased.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 100), Array.Empty())); EnableLateJoinPlayerUpdateSyncPatch = config.Bind("Late Join Settings", "EnableLateJoinPlayerUpgradeSync", false, "Enables Upgrade Sync for Late Joining Players"); LateJoinUpgradeSyncChance = config.Bind(new ConfigDefinition("Late Join Settings", "LateJoinUpgradeSyncChance"), 100, new ConfigDescription("The percentage chance (0-100) that a late joining player will receive each upgrade their team members have.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 100), Array.Empty())); EnableCustomUpgradeSyncing = config.Bind("Extra Sync Settings", "EnableCustomUpgradeSyncing", true, "Enables Custom Upgrade Syncing for Modded Upgrades (may cause issues with some mods)"); } } }