Decompiled source of SharedUpgradesPlus v1.5.1

SharedUpgradesPlus.dll

Decompiled a week ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using ExitGames.Client.Photon;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using Photon.Realtime;
using SharedUpgradesPlus.Configuration;
using SharedUpgradesPlus.Models;
using SharedUpgradesPlus.Patches;
using SharedUpgradesPlus.Services;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp")]
[assembly: AssemblyCompany("Vippy")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.5.1.0")]
[assembly: AssemblyInformationalVersion("1.5.1+2aaab7f626f3fc771c25d35cf5050f4b0c5cb54a")]
[assembly: AssemblyProduct("SharedUpgradesPlus")]
[assembly: AssemblyTitle("SharedUpgradesPlus")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.5.1.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[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]
	[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;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace SharedUpgradesPlus
{
	internal static class BuildInfo
	{
		public const string Version = "1.5.1";
	}
	[BepInPlugin("Vippy.SharedUpgradesPlus", "SharedUpgradesPlus", "1.5.1")]
	public class SharedUpgradesPlus : BaseUnityPlugin
	{
		internal static SharedUpgradesPlus Instance { get; private set; }

		internal static ManualLogSource Logger => Instance.BaseLogger;

		private ManualLogSource BaseLogger => ((BaseUnityPlugin)this).Logger;

		internal Harmony? Harmony { get; set; }

		private void Awake()
		{
			Instance = this;
			((Component)this).gameObject.transform.parent = null;
			((Object)((Component)this).gameObject).hideFlags = (HideFlags)61;
			PluginConfig.Init(((BaseUnityPlugin)this).Config);
			Patch();
			Logger.LogInfo((object)$"{((BaseUnityPlugin)this).Info.Metadata.GUID} v{((BaseUnityPlugin)this).Info.Metadata.Version} has loaded! LogLevel={PluginConfig.LoggingLevel.Value}");
		}

		internal static void LogAlways(string msg)
		{
			Logger.LogInfo((object)msg);
		}

		internal static void LogInfo(string msg)
		{
			if (ConfigService.IsDebugLoggingEnabled())
			{
				Logger.LogInfo((object)msg);
			}
		}

		internal static void LogVerbose(string msg)
		{
			if (ConfigService.IsVerboseLoggingEnabled())
			{
				Logger.LogDebug((object)msg);
			}
		}

		internal void Patch()
		{
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Expected O, but got Unknown
			//IL_0025: Expected O, but got Unknown
			if (Harmony == null)
			{
				Harmony val = new Harmony(((BaseUnityPlugin)this).Info.Metadata.GUID);
				Harmony val2 = val;
				Harmony = val;
			}
			Harmony.PatchAll();
			PatchRepoLib();
		}

		private void PatchRepoLib()
		{
			MethodInfo setLevelMethod = RepoLibInterop.SetLevelMethod;
			MethodInfo applyUpgradeMethod = RepoLibInterop.ApplyUpgradeMethod;
			if (setLevelMethod == null || applyUpgradeMethod == null)
			{
				Logger.LogInfo((object)"REPOLib not detected; skipping modded upgrade patches.");
				return;
			}
			Harmony.Patch((MethodBase)setLevelMethod, HarmonyMethodFor("SetLevelPrefix"), HarmonyMethodFor("SetLevelPostfix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			Harmony.Patch((MethodBase)applyUpgradeMethod, HarmonyMethodFor("ApplyUpgradePrefix"), HarmonyMethodFor("ApplyUpgradePostfix"), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			Logger.LogInfo((object)"Patched REPOLib PlayerUpgrade.SetLevel and ApplyUpgrade.");
		}

		private static HarmonyMethod HarmonyMethodFor(string name)
		{
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Expected O, but got Unknown
			return new HarmonyMethod(typeof(PlayerUpgradePatches).GetMethod(name, BindingFlags.Static | BindingFlags.Public) ?? throw new MissingMethodException(typeof(PlayerUpgradePatches).FullName, name));
		}
	}
}
namespace SharedUpgradesPlus.Services
{
	public static class CompatibilityService
	{
		private const string MoreUpgradesGuid = "bulletbot.moreupgrades";

		private static ConfigFile? _moreUpgradesConfig;

		private static bool _resolved;

		private static readonly Dictionary<string, bool> _sharedByMoreUpgrades = new Dictionary<string, bool>();

		internal static void InvalidateCache()
		{
			_sharedByMoreUpgrades.Clear();
		}

		public static bool IsSharedByMoreUpgrades(string upgradeKey)
		{
			if (!ConfigService.IsAvoidMoreUpgradesDoubleShareEnabled())
			{
				return false;
			}
			if (_sharedByMoreUpgrades.TryGetValue(upgradeKey, out var value))
			{
				return value;
			}
			bool flag = CheckMoreUpgradesConfig(upgradeKey);
			_sharedByMoreUpgrades[upgradeKey] = flag;
			return flag;
		}

		private static bool CheckMoreUpgradesConfig(string upgradeKey)
		{
			ConfigFile val = ResolveConfig();
			if (val == null)
			{
				return false;
			}
			string text = LettersOnly(upgradeKey);
			ConfigEntry<bool> val2 = default(ConfigEntry<bool>);
			foreach (ConfigDefinition key in val.Keys)
			{
				if (!(key.Key != "Allow Team Upgrades") || !(key.Key != "Sync Host Upgrades"))
				{
					string text2 = LettersOnly(key.Section);
					if (text2.Length != 0 && text.Contains(text2) && val.TryGetEntry<bool>(key, ref val2) && val2.Value)
					{
						SharedUpgradesPlus.LogInfo("[Compat] MoreUpgrades shares '" + key.Section + "' itself (" + key.Key + "=true), leaving " + upgradeKey + " to it.");
						return true;
					}
				}
			}
			return false;
		}

		private static ConfigFile? ResolveConfig()
		{
			if (_resolved)
			{
				return _moreUpgradesConfig;
			}
			_resolved = true;
			if (Chainloader.PluginInfos.TryGetValue("bulletbot.moreupgrades", out var value) && (Object)(object)value.Instance != (Object)null)
			{
				_moreUpgradesConfig = value.Instance.Config;
				SharedUpgradesPlus.LogInfo("[Compat] MoreUpgrades detected, double-share avoidance active.");
			}
			return _moreUpgradesConfig;
		}

		private static string LettersOnly(string value)
		{
			StringBuilder stringBuilder = new StringBuilder(value.Length);
			foreach (char c in value)
			{
				if (char.IsLetterOrDigit(c))
				{
					stringBuilder.Append(char.ToLowerInvariant(c));
				}
			}
			return stringBuilder.ToString();
		}
	}
	public static class ConfigService
	{
		private static readonly Dictionary<string, ConfigEntry<bool>> upgradeToggles = new Dictionary<string, ConfigEntry<bool>>();

		private static readonly Dictionary<string, ConfigEntry<int>> limitSliders = new Dictionary<string, ConfigEntry<int>>();

		private static readonly HashSet<string> _disabledByDefault = new HashSet<string> { "playerUpgradeObjectValue", "playerUpgradeObjectDurability" };

		public static bool IsSharedUpgradesEnabled()
		{
			return PluginConfig.EnableSharedUpgrades.Value;
		}

		public static bool IsAvoidMoreUpgradesDoubleShareEnabled()
		{
			return PluginConfig.AvoidMoreUpgradesDoubleShare.Value;
		}

		public static bool IsModdedUpgradesEnabled()
		{
			return PluginConfig.EnableModdedUpgrades.Value;
		}

		public static bool IsLateJoinSyncEnabled()
		{
			return PluginConfig.EnableLateJoinSync.Value;
		}

		public static bool IsSharedUpgradeHealEnabled()
		{
			return PluginConfig.EnableSharedUpgradeHeal.Value;
		}

		public static bool IsShareNotificationEnabled()
		{
			return PluginConfig.EnableShareNotification.Value;
		}

		public static int SharedUpgradesChancePercentage()
		{
			return PluginConfig.SharedUpgradeChance.Value;
		}

		public static bool RollSharedUpgradesChance()
		{
			return Roll(PluginConfig.SharedUpgradeChance.Value);
		}

		public static bool IsWholeTeamChance()
		{
			return PluginConfig.SharedUpgradeChanceMode.Value == "Whole team";
		}

		private static bool Roll(int chance)
		{
			return Random.Range(0, 100) < chance;
		}

		public static bool IsDebugLoggingEnabled()
		{
			return PluginConfig.LoggingLevel.Value >= VerbosityLevel.Debug;
		}

		public static bool IsVerboseLoggingEnabled()
		{
			return PluginConfig.LoggingLevel.Value >= VerbosityLevel.Verbose;
		}

		public static bool IsUpgradeEnabled(string upgradeKey)
		{
			if (upgradeToggles.TryGetValue(upgradeKey, out ConfigEntry<bool> value))
			{
				return value.Value;
			}
			return true;
		}

		public static int UpgradeShareLimit(string upgradeKey)
		{
			if (limitSliders.TryGetValue(upgradeKey, out ConfigEntry<int> value))
			{
				return value.Value;
			}
			return 0;
		}

		public static void LoadModsIntoConfig()
		{
			if (PluginConfig.ConfigFile != null)
			{
				RegisterToggles(RegistryService.Instance.VanillaUpgrades, "Vanilla Upgrades");
				RegisterToggles(RegistryService.Instance.ModdedUpgrades, "Modded Upgrades");
			}
		}

		private static void RegisterToggles(IEnumerable<Upgrade> upgrades, string section)
		{
			//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00aa: Expected O, but got Unknown
			foreach (Upgrade upgrade in upgrades)
			{
				if (!upgradeToggles.ContainsKey(upgrade.Name))
				{
					upgradeToggles[upgrade.Name] = PluginConfig.ConfigFile.Bind<bool>(section, upgrade.CleanName, !_disabledByDefault.Contains(upgrade.Name), "Enable sharing for " + upgrade.CleanName);
					limitSliders[upgrade.Name] = PluginConfig.ConfigFile.Bind<int>(section, upgrade.CleanName + " Share Limit", 0, new ConfigDescription("Others won't receive this upgrade past this level (0 = unlimited)", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>()));
				}
			}
		}
	}
	public static class DiscoveryService
	{
		public static DiscoveredUpgradesResult DiscoveredUpgrades(StatsManager statsManager)
		{
			HashSet<string> hashSet = new HashSet<string>();
			HashSet<string> hashSet2 = new HashSet<string>();
			foreach (string item in statsManager.dictionaryOfDictionaries.Keys.Where((string key) => key.StartsWith("playerUpgrade")))
			{
				if (AccessTools.Field(typeof(StatsManager), item) != null)
				{
					hashSet.Add(item);
				}
				else
				{
					hashSet2.Add(item);
				}
			}
			foreach (string moddedUpgradeKey in RepoLibInterop.GetModdedUpgradeKeys())
			{
				if (!hashSet.Contains(moddedUpgradeKey))
				{
					hashSet2.Add(moddedUpgradeKey);
				}
			}
			return new DiscoveredUpgradesResult(hashSet, hashSet2);
		}
	}
	public static class DistributionService
	{
		private static int _distributingDepth;

		public static bool IsDistributing => _distributingDepth > 0;

		internal static void EnterDistributing()
		{
			_distributingDepth++;
		}

		internal static void ExitDistributing()
		{
			_distributingDepth = Math.Max(0, _distributingDepth - 1);
		}

		public static void DistributeUpgrade(UpgradeContext context, string upgradeKey, int difference)
		{
			SharedUpgradesPlus.LogVerbose($"[Distribute] {context.PlayerName} bought {upgradeKey} (+{difference})");
			int num = ConfigService.UpgradeShareLimit(upgradeKey);
			PhotonView component = ((Component)PunManager.instance).GetComponent<PhotonView>();
			if ((Object)(object)component == (Object)null)
			{
				SharedUpgradesPlus.Logger.LogWarning((object)"[Distribute] PhotonView not found on PunManager, can't distribute.");
				return;
			}
			bool flag = RegistryService.Instance.IsVanilla(upgradeKey);
			if (!flag && !ConfigService.IsModdedUpgradesEnabled())
			{
				SharedUpgradesPlus.LogInfo("[Distribute] " + upgradeKey + " is modded and modded upgrades are off, skipping.");
				return;
			}
			if (!ConfigService.IsUpgradeEnabled(upgradeKey))
			{
				SharedUpgradesPlus.LogInfo("[Distribute] " + upgradeKey + " is disabled in config, skipping.");
				return;
			}
			if (!flag && CompatibilityService.IsSharedByMoreUpgrades(upgradeKey))
			{
				SharedUpgradesPlus.LogInfo("[Distribute] " + upgradeKey + ": MoreUpgrades already team-shares it, skipping to avoid a double apply.");
				return;
			}
			string text = (flag ? new Upgrade(upgradeKey).CleanName : null);
			List<PlayerAvatar> list = SemiFunc.PlayerGetAll();
			int num2 = ConfigService.SharedUpgradesChancePercentage();
			bool flag2 = ConfigService.IsWholeTeamChance();
			SharedUpgradesPlus.LogVerbose(string.Format("[Distribute] {0} (+{1}): {2} player(s), limit={3}, chance={4}%, mode={5}", upgradeKey, difference, list.Count, num, num2, flag2 ? "whole team" : "each player"));
			if (flag2 && !ConfigService.RollSharedUpgradesChance())
			{
				SharedUpgradesPlus.LogAlways($"[Distribute] {upgradeKey}: team roll failed ({num2}%), nobody receives it.");
				HealBuyer(context, upgradeKey, difference);
				return;
			}
			EnterDistributing();
			int num3 = 0;
			int num4 = 0;
			try
			{
				foreach (PlayerAvatar item in list)
				{
					if ((Object)(object)item == (Object)null || (Object)(object)item.photonView == (Object)null || item.photonView.ViewID == context.ViewID)
					{
						continue;
					}
					string steamID = item.steamID;
					if (string.IsNullOrEmpty(steamID))
					{
						continue;
					}
					int value = 0;
					if (StatsManager.instance.dictionaryOfDictionaries.TryGetValue(upgradeKey, out var value2))
					{
						value2.TryGetValue(steamID, out value);
					}
					SharedUpgradesPlus.LogVerbose($"[Distribute]   {item.playerName}: level={value}, limit={num}");
					if (num > 0 && num <= value)
					{
						SharedUpgradesPlus.LogInfo($"[Distribute]   {item.playerName} hit share limit ({num}), skipping.");
						num4++;
						continue;
					}
					if (!flag2 && !ConfigService.RollSharedUpgradesChance())
					{
						SharedUpgradesPlus.LogInfo($"[Distribute]   {item.playerName} roll failed ({num2}%), skipping.");
						num4++;
						continue;
					}
					int num5 = value + difference;
					SharedUpgradesPlus.LogAlways($"[Distribute]   {item.playerName}: {value} -> {num5} (+{difference})");
					if (flag)
					{
						component.RPC("TesterUpgradeCommandRPC", (RpcTarget)0, new object[3] { steamID, text, difference });
					}
					else if (!RepoLibInterop.TrySetLevel(upgradeKey, steamID, num5))
					{
						SharedUpgradesPlus.Logger.LogWarning((object)("[Distribute]   " + upgradeKey + ": SetLevel failed for " + item.playerName + ", falling back to UpdateStatRPC."));
						component.RPC("UpdateStatRPC", (RpcTarget)0, new object[3] { upgradeKey, steamID, num5 });
					}
					num3++;
				}
			}
			finally
			{
				ExitDistributing();
			}
			SharedUpgradesPlus.LogVerbose($"[Distribute] done {upgradeKey}: sent={num3}, skipped={num4}");
			HealBuyer(context, upgradeKey, difference);
		}

		private static void HealBuyer(UpgradeContext context, string upgradeKey, int difference)
		{
			if (upgradeKey != "playerUpgradeHealth" || !ConfigService.IsSharedUpgradeHealEnabled())
			{
				return;
			}
			PlayerAvatar val = SemiFunc.PlayerAvatarGetFromSteamID(context.SteamID);
			if (!((Object)(object)val == (Object)null))
			{
				int num = val.playerHealth.maxHealth + 20 * difference - val.playerHealth.health;
				SharedUpgradesPlus.LogVerbose($"[Distribute] healing {context.PlayerName}: max={val.playerHealth.maxHealth}, current={val.playerHealth.health}, healing={num}");
				if (num > 0)
				{
					val.playerHealth.HealOther(num, false);
				}
			}
		}
	}
	public static class EffectsService
	{
		public static void PlayShareEffect(PlayerAvatar player)
		{
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			if (player.isLocal)
			{
				StatsUI.instance.Fetch();
				StatsUI.instance.ShowStats();
				CameraGlitch.Instance.PlayUpgrade();
			}
			else
			{
				GameDirector.instance.CameraImpact.ShakeDistance(5f, 1f, 6f, ((Component)player).transform.position, 0.2f);
			}
			if (!GameManager.Multiplayer() || PhotonNetwork.IsMasterClient)
			{
				player.playerHealth.MaterialEffectOverride((Effect)0);
			}
		}
	}
	public class NetworkCallbackService : MonoBehaviourPunCallbacks
	{
		private readonly HashSet<Player> _pendingSync = new HashSet<Player>();

		public static NetworkCallbackService? Instance { get; private set; }

		public override void OnJoinedRoom()
		{
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_0037: Expected O, but got Unknown
			//IL_0038: Expected O, but got Unknown
			SharedUpgradesPlus.LogVerbose($"OnJoinedRoom (isMaster={PhotonNetwork.IsMasterClient})");
			try
			{
				if (PhotonNetwork.IsMasterClient)
				{
					Hashtable val = new Hashtable();
					((Dictionary<object, object>)val).Add((object)"su__v1", (object)"1.5.1");
					Hashtable val2 = val;
					PhotonNetwork.CurrentRoom.SetCustomProperties(val2, (Hashtable)null, (WebFlags)null);
					SharedUpgradesPlus.LogVerbose("Set room property: su__v1=1.5.1");
				}
			}
			catch (Exception ex)
			{
				SharedUpgradesPlus.Logger.LogWarning((object)("Couldn't set room properties: " + ex.Message));
			}
		}

		public override void OnPlayerEnteredRoom(Player newPlayer)
		{
			SharedUpgradesPlus.LogVerbose($"OnPlayerEnteredRoom: {newPlayer.NickName} (isMaster={SemiFunc.IsMasterClientOrSingleplayer()} lateJoin={ConfigService.IsLateJoinSyncEnabled()})");
			if (ConfigService.IsLateJoinSyncEnabled() && ConfigService.IsSharedUpgradesEnabled() && SemiFunc.IsMasterClientOrSingleplayer())
			{
				_pendingSync.Add(newPlayer);
				SharedUpgradesPlus.LogAlways($"Deferred sync: {newPlayer.NickName} joined, queued. ({_pendingSync.Count} pending)");
			}
		}

		public override void OnPlayerLeftRoom(Player otherPlayer)
		{
			bool flag = _pendingSync.Remove(otherPlayer);
			SharedUpgradesPlus.LogVerbose($"OnPlayerLeftRoom: {otherPlayer.NickName} (was pending: {flag}, pending count: {_pendingSync.Count})");
		}

		public static bool IsPlayerPendingSync(Player player)
		{
			if ((Object)(object)Instance != (Object)null)
			{
				return Instance._pendingSync.Contains(player);
			}
			return false;
		}

		public IEnumerator LateSyncPlayer(PlayerAvatar avatar, string steamID, Dictionary<string, int> teamSnapshot)
		{
			yield return SyncService.ApplyTeamSnapshot(avatar, steamID, teamSnapshot);
			_pendingSync.Remove(avatar.photonView.Owner);
		}

		private void Awake()
		{
			Instance = this;
			CatchUpExistingPlayers();
		}

		private void CatchUpExistingPlayers()
		{
			if (ConfigService.IsLateJoinSyncEnabled() && ConfigService.IsSharedUpgradesEnabled() && SemiFunc.IsMasterClientOrSingleplayer())
			{
				Player[] playerListOthers = PhotonNetwork.PlayerListOthers;
				foreach (Player val in playerListOthers)
				{
					_pendingSync.Add(val);
					SharedUpgradesPlus.LogAlways($"Catch-up sync: {val.NickName} was already in room, queued. ({_pendingSync.Count} pending)");
				}
			}
		}
	}
	public sealed class RegistryService
	{
		private readonly HashSet<Upgrade> vanillaUpgrades;

		private readonly HashSet<Upgrade> moddedUpgrades;

		private static readonly RegistryService instance = new RegistryService();

		public IReadOnlyCollection<Upgrade> VanillaUpgrades => vanillaUpgrades;

		public IReadOnlyCollection<Upgrade> ModdedUpgrades => moddedUpgrades;

		public static RegistryService Instance => instance;

		private RegistryService()
		{
			vanillaUpgrades = new HashSet<Upgrade>();
			moddedUpgrades = new HashSet<Upgrade>();
		}

		public void RegisterAll(DiscoveredUpgradesResult result)
		{
			vanillaUpgrades.UnionWith(result.Vanilla.Select(MakeUpgradeFromKey));
			moddedUpgrades.UnionWith(result.Modded.Select(MakeUpgradeFromKey));
			CompatibilityService.InvalidateCache();
			SharedUpgradesPlus.Logger.LogInfo((object)$"Discovered {vanillaUpgrades.Count} vanilla and {moddedUpgrades.Count} modded upgrade(s).");
			if (result.Vanilla.Count > 0)
			{
				SharedUpgradesPlus.LogVerbose("Vanilla: " + string.Join(", ", result.Vanilla));
			}
			if (result.Modded.Count > 0)
			{
				SharedUpgradesPlus.LogVerbose("Modded: " + string.Join(", ", result.Modded));
			}
		}

		public void Clear()
		{
			SharedUpgradesPlus.LogVerbose($"Registry cleared ({vanillaUpgrades.Count} vanilla, {moddedUpgrades.Count} modded).");
			vanillaUpgrades.Clear();
			moddedUpgrades.Clear();
		}

		public bool IsVanilla(string key)
		{
			return vanillaUpgrades.Any((Upgrade upgrade) => upgrade.Name.Equals(key));
		}

		public bool IsRegistered(string key)
		{
			if (!IsVanilla(key))
			{
				return moddedUpgrades.Any((Upgrade upgrade) => upgrade.Name.Equals(key));
			}
			return true;
		}

		private Upgrade MakeUpgradeFromKey(string key)
		{
			return new Upgrade(key);
		}
	}
	internal static class RepoLibInterop
	{
		private const string KeyPrefix = "playerUpgrade";

		private const BindingFlags MemberFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;

		private static bool _resolved;

		private static Type? _playerUpgradeType;

		private static FieldInfo? _upgradeIdField;

		private static FieldInfo? _playerDictionaryField;

		private static MethodInfo? _applyUpgradeMethod;

		private static MethodInfo? _setLevelMethod;

		private static FieldInfo? _playerUpgradesDictField;

		private static PropertyInfo? _playerUpgradesProp;

		public static MethodInfo? ApplyUpgradeMethod
		{
			get
			{
				Resolve();
				return _applyUpgradeMethod;
			}
		}

		public static MethodInfo? SetLevelMethod
		{
			get
			{
				Resolve();
				return _setLevelMethod;
			}
		}

		public static HashSet<string> GetModdedUpgradeKeys()
		{
			HashSet<string> hashSet = new HashSet<string>();
			Resolve();
			if (_playerUpgradesProp == null || _upgradeIdField == null)
			{
				return hashSet;
			}
			try
			{
				if (!(_playerUpgradesProp.GetValue(null) is IEnumerable enumerable))
				{
					return hashSet;
				}
				foreach (object item in enumerable)
				{
					if (item != null && _upgradeIdField.GetValue(item) is string text && !string.IsNullOrEmpty(text))
					{
						hashSet.Add("playerUpgrade" + text);
					}
				}
			}
			catch (Exception ex)
			{
				SharedUpgradesPlus.LogVerbose("[RepoLibInterop] reading PlayerUpgrades failed: " + ex.Message);
			}
			return hashSet;
		}

		public static bool TryReadUpgradeKey(object playerUpgrade, out string upgradeKey)
		{
			upgradeKey = string.Empty;
			Resolve();
			if (_upgradeIdField == null)
			{
				return false;
			}
			if (!(_upgradeIdField.GetValue(playerUpgrade) is string text) || string.IsNullOrEmpty(text))
			{
				return false;
			}
			upgradeKey = "playerUpgrade" + text;
			return true;
		}

		public static int ReadCurrentLevel(object playerUpgrade, string steamId)
		{
			Resolve();
			if (_playerDictionaryField == null)
			{
				return 0;
			}
			if (!(_playerDictionaryField.GetValue(playerUpgrade) is IDictionary dictionary))
			{
				return 0;
			}
			if (!dictionary.Contains(steamId))
			{
				return 0;
			}
			object obj = dictionary[steamId];
			if (obj is int)
			{
				return (int)obj;
			}
			return 0;
		}

		public static bool TrySetLevel(string upgradeKey, string steamId, int level)
		{
			Resolve();
			if (_setLevelMethod == null || _playerUpgradesDictField == null)
			{
				return false;
			}
			if (!upgradeKey.StartsWith("playerUpgrade"))
			{
				return false;
			}
			int length = "playerUpgrade".Length;
			string text = upgradeKey.Substring(length, upgradeKey.Length - length);
			if (!(_playerUpgradesDictField.GetValue(null) is IDictionary dictionary))
			{
				return false;
			}
			if (!dictionary.Contains(text))
			{
				return false;
			}
			object obj = dictionary[text];
			if (obj == null)
			{
				return false;
			}
			try
			{
				_setLevelMethod.Invoke(obj, new object[2] { steamId, level });
				return true;
			}
			catch (Exception ex)
			{
				Exception ex2 = (ex as TargetInvocationException)?.InnerException ?? ex;
				SharedUpgradesPlus.Logger.LogWarning((object)$"[RepoLibInterop] SetLevel({text}, {steamId}, {level}) threw {ex2.GetType().Name}: {ex2.Message}");
				return false;
			}
		}

		private static void Resolve()
		{
			if (_resolved)
			{
				return;
			}
			_resolved = true;
			Type type = AccessTools.TypeByName("REPOLib.Modules.Upgrades");
			if (!(type == null))
			{
				_playerUpgradeType = AccessTools.TypeByName("REPOLib.Modules.PlayerUpgrade");
				if (!(_playerUpgradeType == null))
				{
					_playerUpgradesProp = type.GetProperty("PlayerUpgrades", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
					_playerUpgradesDictField = type.GetField("_playerUpgrades", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
					_upgradeIdField = _playerUpgradeType.GetField("UpgradeId", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
					_playerDictionaryField = _playerUpgradeType.GetField("PlayerDictionary", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
					_applyUpgradeMethod = _playerUpgradeType.GetMethod("ApplyUpgrade", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[2]
					{
						typeof(string),
						typeof(int)
					}, null);
					_setLevelMethod = _playerUpgradeType.GetMethod("SetLevel", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[2]
					{
						typeof(string),
						typeof(int)
					}, null);
				}
			}
		}
	}
	public static class SnapshotService
	{
		public static Dictionary<string, int> SnapshotPlayerStats(string steamID)
		{
			if (string.IsNullOrEmpty(steamID) || StatsManager.instance == null)
			{
				return new Dictionary<string, int>();
			}
			Dictionary<string, int> dictionary = StatsManager.instance.dictionaryOfDictionaries.Where((KeyValuePair<string, Dictionary<string, int>> kvp) => RegistryService.Instance.IsRegistered(kvp.Key)).ToDictionary((KeyValuePair<string, Dictionary<string, int>> kvp) => kvp.Key, (KeyValuePair<string, Dictionary<string, int>> kvp) => kvp.Value.GetValueOrDefault(steamID, 0));
			SharedUpgradesPlus.LogVerbose($"[Snapshot] Player snapshot for {steamID}: {dictionary.Count} upgrade(s).");
			return dictionary;
		}

		public static Dictionary<string, int> SnapshotTeamMaxLevels(string? excludeSteamID = null)
		{
			Dictionary<string, int> dictionary = new Dictionary<string, int>();
			if (StatsManager.instance == null)
			{
				return dictionary;
			}
			foreach (KeyValuePair<string, Dictionary<string, int>> item in StatsManager.instance.dictionaryOfDictionaries.Where((KeyValuePair<string, Dictionary<string, int>> k) => RegistryService.Instance.IsRegistered(k.Key)))
			{
				IEnumerable<int> enumerable;
				if (!string.IsNullOrEmpty(excludeSteamID))
				{
					enumerable = from p in item.Value
						where p.Key != excludeSteamID
						select p.Value;
				}
				else
				{
					IEnumerable<int> values = item.Value.Values;
					enumerable = values;
				}
				IEnumerable<int> source = enumerable;
				dictionary[item.Key] = source.DefaultIfEmpty(0).Max();
			}
			SharedUpgradesPlus.LogVerbose(string.Format("[Snapshot] Team snapshot (exclude={0}): {1} upgrade(s).", excludeSteamID ?? "none", dictionary.Count));
			return dictionary;
		}
	}
	public static class SyncService
	{
		public static IEnumerator ApplyTeamSnapshot(PlayerAvatar player, string steamID, Dictionary<string, int> teamSnapshot)
		{
			if ((Object)(object)StatsManager.instance == (Object)null || (Object)(object)PunManager.instance == (Object)null)
			{
				yield break;
			}
			PhotonView component = ((Component)PunManager.instance).GetComponent<PhotonView>();
			if ((Object)(object)component == (Object)null)
			{
				SharedUpgradesPlus.Logger.LogWarning((object)"[LateJoin] PhotonView not found on PunManager, skipping sync.");
				yield break;
			}
			string playerName = player.playerName;
			int num = ConfigService.SharedUpgradesChancePercentage();
			SharedUpgradesPlus.LogAlways($"[LateJoin] syncing {playerName}: {teamSnapshot.Count} upgrade(s), chance={num}%");
			int num2 = 0;
			int num3 = 0;
			DistributionService.EnterDistributing();
			try
			{
				foreach (KeyValuePair<string, int> item in teamSnapshot)
				{
					int num4 = ConfigService.UpgradeShareLimit(item.Key);
					bool flag = RegistryService.Instance.IsVanilla(item.Key);
					SharedUpgradesPlus.LogVerbose($"[LateJoin]   {item.Key}: teamMax={item.Value}, isVanilla={flag}, limit={num4}");
					if (!flag && !ConfigService.IsModdedUpgradesEnabled())
					{
						SharedUpgradesPlus.LogVerbose("[LateJoin]   " + item.Key + ": skipped (modded upgrades disabled).");
						num3++;
						continue;
					}
					if (!ConfigService.IsUpgradeEnabled(item.Key))
					{
						SharedUpgradesPlus.LogVerbose("[LateJoin]   " + item.Key + ": skipped (disabled in config).");
						num3++;
						continue;
					}
					Dictionary<string, int> value;
					int num5 = (StatsManager.instance.dictionaryOfDictionaries.TryGetValue(item.Key, out value) ? value.GetValueOrDefault(steamID, 0) : 0);
					if (num4 > 0 && num4 <= num5)
					{
						SharedUpgradesPlus.LogInfo($"[LateJoin]   {item.Key}: {playerName} hit share limit ({num4}), skipping.");
						num3++;
						continue;
					}
					int value2 = item.Value;
					int num6 = value2 - num5;
					if (num4 > 0)
					{
						num6 = Math.Min(num6, num4 - num5);
					}
					SharedUpgradesPlus.LogVerbose($"[LateJoin]   {item.Key}: level={num5}, teamMax={item.Value}, diff={num6} (pre-roll)");
					num6 = SimulateRealisticLevelling(num6);
					value2 = num5 + num6;
					if (num6 <= 0)
					{
						SharedUpgradesPlus.LogInfo("[LateJoin]   " + item.Key + ": rolled 0 after chance simulation, skipping.");
						num3++;
						continue;
					}
					if (flag)
					{
						SharedUpgradesPlus.LogVerbose("[LateJoin]   " + item.Key + ": sending TesterUpgradeCommandRPC to " + playerName);
						component.RPC("TesterUpgradeCommandRPC", (RpcTarget)0, new object[3]
						{
							steamID,
							new Upgrade(item.Key).CleanName,
							num6
						});
					}
					else if (!RepoLibInterop.TrySetLevel(item.Key, steamID, value2))
					{
						SharedUpgradesPlus.Logger.LogWarning((object)("[LateJoin]   " + item.Key + ": SetLevel failed for " + playerName + ", falling back to UpdateStatRPC."));
						component.RPC("UpdateStatRPC", (RpcTarget)0, new object[3] { item.Key, steamID, value2 });
					}
					SharedUpgradesPlus.LogVerbose($"[LateJoin]   sent {item.Key} (+{num6}) to {playerName}.");
					num2++;
				}
			}
			finally
			{
				DistributionService.ExitDistributing();
			}
			SharedUpgradesPlus.LogAlways($"[LateJoin] done {playerName}: sent={num2}, skipped={num3}");
		}

		private static int SimulateRealisticLevelling(int value)
		{
			int num = ConfigService.SharedUpgradesChancePercentage();
			if (num >= 100 || value <= 0)
			{
				return value;
			}
			if (ConfigService.IsWholeTeamChance())
			{
				return value;
			}
			int num2 = 0;
			for (int i = 0; i < value; i++)
			{
				if (ConfigService.RollSharedUpgradesChance())
				{
					num2++;
				}
			}
			SharedUpgradesPlus.LogVerbose($"[LateJoin] roll simulation: input={value}, chance={num}%, result={num2}");
			return num2;
		}
	}
	internal class WatermarkService : MonoBehaviour
	{
		internal const string RoomKey = "su__v1";

		private static string Version = "UNKNOWN";

		private static readonly string? OwnerID = LoadOwnerID();

		private bool show;

		private bool polling;

		private Object? lastLevel;

		private string? lastRoom;

		private GUIStyle? style;

		private static string? LoadOwnerID()
		{
			try
			{
				string path = Path.Combine(Paths.ConfigPath, "SharedUpgradesPlus.owner");
				if (!File.Exists(path))
				{
					return null;
				}
				return File.ReadAllText(path).Trim();
			}
			catch
			{
				return null;
			}
		}

		private void Update()
		{
			if ((Object)(object)RunManager.instance == (Object)null)
			{
				return;
			}
			Level levelCurrent = RunManager.instance.levelCurrent;
			Room currentRoom = PhotonNetwork.CurrentRoom;
			string text = ((currentRoom != null) ? currentRoom.Name : null);
			if ((Object)(object)levelCurrent == lastLevel && text == lastRoom)
			{
				return;
			}
			lastLevel = (Object?)(object)levelCurrent;
			lastRoom = text;
			if (text == null)
			{
				show = false;
			}
			if ((Object)(object)levelCurrent == (Object)(object)RunManager.instance.levelLobbyMenu)
			{
				show = false;
				if (!polling)
				{
					((MonoBehaviour)this).StartCoroutine(Poll());
				}
			}
		}

		private IEnumerator Poll()
		{
			polling = true;
			float elapsed = 0f;
			while (elapsed < 10f)
			{
				yield return (object)new WaitForSeconds(1f);
				elapsed += 1f;
				try
				{
					if (!PhotonNetwork.InRoom)
					{
						continue;
					}
					if (string.IsNullOrEmpty(OwnerID))
					{
						break;
					}
					PlayerAvatar val = null;
					foreach (PlayerAvatar item in SemiFunc.PlayerGetAll())
					{
						if ((Object)(object)item?.photonView != (Object)null && item.photonView.IsMine)
						{
							val = item;
							break;
						}
					}
					if ((Object)(object)val == (Object)null)
					{
						continue;
					}
					if (val.steamID != OwnerID)
					{
						break;
					}
					bool isMasterClient = PhotonNetwork.IsMasterClient;
					bool flag = ((Dictionary<object, object>)(object)((RoomInfo)PhotonNetwork.CurrentRoom).CustomProperties).ContainsKey((object)"su__v1");
					if (!(isMasterClient || flag))
					{
						continue;
					}
					if (((Dictionary<object, object>)(object)((RoomInfo)PhotonNetwork.CurrentRoom).CustomProperties).TryGetValue((object)"su__v1", out object value))
					{
						Version = (value as string) ?? "UNKNOWN";
					}
					show = !isMasterClient && flag;
					break;
				}
				catch
				{
				}
			}
			polling = false;
		}

		private void OnGUI()
		{
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0053: Expected O, but got Unknown
			if (!show)
			{
				return;
			}
			try
			{
				if (style == null)
				{
					GUIStyle val = new GUIStyle(GUI.skin.label)
					{
						fontSize = 24
					};
					val.normal.textColor = new Color(1f, 1f, 1f, 0.15f);
					style = val;
				}
				GUI.Label(new Rect(6f, (float)(Screen.height - 72), 160f, 56f), "SUP: " + Version, style);
			}
			catch
			{
			}
		}
	}
}
namespace SharedUpgradesPlus.Patches
{
	[HarmonyPatch(typeof(PlayerTumble), "SetupDone")]
	internal class PlayerTumbleSetupDonePatch
	{
		[HarmonyPostfix]
		public static void Postfix(PlayerTumble __instance)
		{
			if (SemiFunc.IsMasterClientOrSingleplayer() && !string.IsNullOrEmpty(__instance.playerAvatar.steamID) && NetworkCallbackService.IsPlayerPendingSync(__instance.playerAvatar.photonView.Owner) && ConfigService.IsLateJoinSyncEnabled() && ConfigService.IsSharedUpgradesEnabled())
			{
				SharedUpgradesPlus.LogAlways("[LateJoin] SetupDone fired for " + __instance.playerAvatar.steamID + ", triggering sync.");
				Dictionary<string, int> teamSnapshot = SnapshotService.SnapshotTeamMaxLevels(__instance.playerAvatar.steamID);
				if ((Object)(object)NetworkCallbackService.Instance == (Object)null)
				{
					SharedUpgradesPlus.Logger.LogError((object)"NetworkCallbackService instance is null. Cannot sync player stats.");
				}
				else
				{
					((MonoBehaviour)NetworkCallbackService.Instance).StartCoroutine(NetworkCallbackService.Instance.LateSyncPlayer(__instance.playerAvatar, __instance.playerAvatar.steamID, teamSnapshot));
				}
			}
		}
	}
	[HarmonyPatch(typeof(PunManager), "TesterUpgradeCommandRPC")]
	internal class PlayerUpgradeEffectPatch
	{
		[HarmonyPostfix]
		public static void Postfix(string _steamID, string upgradeName, int upgradeNum, PhotonMessageInfo _info)
		{
			bool flag = upgradeName == "Health";
			PlayerAvatar val = SemiFunc.PlayerAvatarGetFromSteamID(_steamID);
			if ((Object)(object)val == (Object)null)
			{
				SharedUpgradesPlus.Logger.LogError((object)("[Effects] TesterUpgradeCommandRPC fired for " + _steamID + " but no PlayerAvatar found, skipping effects."));
				return;
			}
			SharedUpgradesPlus.LogVerbose($"[Effects] {val.playerName} got {upgradeName} x{upgradeNum} (local={val.isLocal})");
			if (ConfigService.IsShareNotificationEnabled())
			{
				EffectsService.PlayShareEffect(val);
			}
			if (flag && SemiFunc.IsMasterClientOrSingleplayer() && ConfigService.IsSharedUpgradeHealEnabled())
			{
				int num = val.playerHealth.maxHealth + 20 * upgradeNum - val.playerHealth.health;
				SharedUpgradesPlus.LogVerbose($"[Effects] healing {val.playerName}: max={val.playerHealth.maxHealth}, current={val.playerHealth.health}, healing={num}");
				if (num > 0)
				{
					val.playerHealth.HealOther(num, false);
				}
			}
		}
	}
	internal static class PlayerUpgradePatches
	{
		[ThreadStatic]
		private static int? _stashedPrior;

		public static void SetLevelPrefix(object __instance, string steamId)
		{
			_stashedPrior = RepoLibInterop.ReadCurrentLevel(__instance, steamId);
		}

		public static void SetLevelPostfix()
		{
			_stashedPrior = null;
		}

		public static void ApplyUpgradePrefix(object __instance, string steamId, out int __state)
		{
			if (_stashedPrior.HasValue)
			{
				__state = _stashedPrior.Value;
				_stashedPrior = null;
			}
			else
			{
				__state = RepoLibInterop.ReadCurrentLevel(__instance, steamId);
			}
		}

		public static void ApplyUpgradePostfix(object __instance, string steamId, int level, int __state)
		{
			if (!ConfigService.IsSharedUpgradesEnabled() || !ConfigService.IsModdedUpgradesEnabled() || !RepoLibInterop.TryReadUpgradeKey(__instance, out string upgradeKey) || !RegistryService.Instance.IsRegistered(upgradeKey) || RegistryService.Instance.IsVanilla(upgradeKey) || !ConfigService.IsUpgradeEnabled(upgradeKey))
			{
				return;
			}
			int num = level - __state;
			PlayerAvatar val = SemiFunc.PlayerAvatarGetFromSteamID(steamId);
			SharedUpgradesPlus.LogVerbose(string.Format("[Modded] {0} ({1}): {2} -> {3} (+{4}), player={5}, distributing={6}", upgradeKey, steamId, __state, level, num, val?.playerName ?? "not found", DistributionService.IsDistributing));
			if ((Object)(object)val != (Object)null && num > 0 && ConfigService.IsShareNotificationEnabled())
			{
				EffectsService.PlayShareEffect(val);
			}
			if (num > 0 && SemiFunc.IsMasterClientOrSingleplayer() && !DistributionService.IsDistributing)
			{
				if ((Object)(object)val == (Object)null || (Object)(object)val.photonView == (Object)null)
				{
					SharedUpgradesPlus.Logger.LogWarning((object)("[Modded] no PlayerAvatar for " + steamId + ", can't distribute " + upgradeKey + "."));
				}
				else
				{
					SharedUpgradesPlus.LogAlways($"[Modded] {val.playerName} bought {upgradeKey} (+{num}), distributing...");
					DistributionService.DistributeUpgrade(new UpgradeContext(steamId, val.photonView.ViewID, val.playerName, new Dictionary<string, int>()), upgradeKey, num);
				}
			}
		}
	}
	[HarmonyPatch(typeof(StatsManager), "RunStartStats")]
	internal class REPOLibSyncPatch
	{
		private static readonly Type _upgradeType = AccessTools.TypeByName("REPOLib.Modules.Upgrades");

		private static readonly Type _playerUpgradeType = AccessTools.TypeByName("REPOLib.Modules.PlayerUpgrade");

		private static readonly FieldInfo? _playerDictionaryField = ((_playerUpgradeType != null) ? AccessTools.Field(_playerUpgradeType, "PlayerDictionary") : null);

		private static readonly FieldInfo? _playerUpgradesField = ((_upgradeType != null) ? AccessTools.Field(_upgradeType, "_playerUpgrades") : null);

		[HarmonyPostfix]
		[HarmonyPriority(0)]
		public static void Postfix()
		{
			if (_playerDictionaryField == null || _playerUpgradesField == null || (Object)(object)StatsManager.instance == (Object)null || !(_playerUpgradesField.GetValue(null) is IDictionary dictionary))
			{
				return;
			}
			SharedUpgradesPlus.LogVerbose($"Syncing {dictionary.Count} REPOLib upgrade(s) to StatsManager.");
			int num = 0;
			foreach (DictionaryEntry item in dictionary)
			{
				if (item.Value != null)
				{
					string text = $"playerUpgrade{item.Key}";
					if (StatsManager.instance.dictionaryOfDictionaries.TryGetValue(text, out var value))
					{
						_playerDictionaryField.SetValue(item.Value, value);
						SharedUpgradesPlus.LogInfo("Synced PlayerDictionary for " + text + ".");
						num++;
					}
				}
			}
			SharedUpgradesPlus.LogVerbose($"REPOLib sync done: {num}/{dictionary.Count} upgrade(s).");
		}
	}
	[HarmonyPatch(typeof(ItemUpgrade), "PlayerUpgrade")]
	internal class SharedUpgradesPatch
	{
		private static PlayerAvatar? GetUpgradePlayer(ItemUpgrade instance, out int viewID)
		{
			viewID = 0;
			ItemToggle itemToggle = instance.itemToggle;
			if (itemToggle == null || !itemToggle.toggleState)
			{
				return null;
			}
			viewID = instance.itemToggle.playerTogglePhotonID;
			return SemiFunc.PlayerAvatarGetFromPhotonID(viewID);
		}

		[HarmonyPrefix]
		public static void Prefix(ItemUpgrade __instance, out UpgradeContext? __state)
		{
			__state = null;
			if (!ConfigService.IsSharedUpgradesEnabled() || !SemiFunc.IsMasterClientOrSingleplayer())
			{
				return;
			}
			int viewID;
			PlayerAvatar upgradePlayer = GetUpgradePlayer(__instance, out viewID);
			if (upgradePlayer == null)
			{
				SharedUpgradesPlus.LogVerbose("[Purchase] upgrade interaction fired but couldn't find a player, skipping.");
				return;
			}
			string steamID = upgradePlayer.steamID;
			if (!string.IsNullOrEmpty(steamID))
			{
				string text = null;
				ItemAttributes itemAttributes = __instance.itemAttributes;
				if (itemAttributes != null && (Object)(object)itemAttributes.item != (Object)null)
				{
					text = ((Object)itemAttributes.item).name;
				}
				SharedUpgradesPlus.LogVerbose("[Purchase] " + upgradePlayer.playerName + " is buying '" + text + "'");
				__state = new UpgradeContext(steamID, playerName: upgradePlayer.playerName, viewID: viewID, levelsBefore: SnapshotService.SnapshotPlayerStats(steamID), itemName: text);
			}
		}

		[HarmonyPostfix]
		public static void Postfix(UpgradeContext? __state)
		{
			if (!SemiFunc.IsMasterClientOrSingleplayer() || __state == null)
			{
				return;
			}
			SharedUpgradesPlus.LogVerbose("[Purchase] checking what " + __state.PlayerName + " just bought (item='" + __state.ItemName + "')");
			bool flag = false;
			foreach (KeyValuePair<string, Dictionary<string, int>> item in StatsManager.instance.dictionaryOfDictionaries.Where((KeyValuePair<string, Dictionary<string, int>> key) => RegistryService.Instance.IsRegistered(key.Key)))
			{
				item.Value.TryGetValue(__state.SteamID, out var value);
				__state.LevelsBefore.TryGetValue(item.Key, out var value2);
				SharedUpgradesPlus.LogVerbose($"[Purchase]   {item.Key}: {value2} -> {value}");
				if (value > value2)
				{
					int num = value - value2;
					flag = true;
					SharedUpgradesPlus.LogAlways($"[Purchase] {__state.PlayerName} bought {item.Key} (+{num}), distributing...");
					DistributionService.DistributeUpgrade(__state, item.Key, num);
				}
			}
			SharedUpgradesPlus.LogVerbose($"[Purchase] vanilla scan done, distributed={flag}");
			if (!flag && __state.ItemName != null && ConfigService.IsModdedUpgradesEnabled())
			{
				SharedUpgradesPlus.LogVerbose("[Purchase] no vanilla upgrades changed, checking modded match for '" + __state.ItemName + "'");
				string text = MatchItemNameToModdedUpgrade(__state.ItemName);
				if (text != null)
				{
					SharedUpgradesPlus.LogInfo("[Purchase] " + __state.PlayerName + " (" + __state.SteamID + ") bought modded " + text + " (+1), distributing...");
					DistributionService.DistributeUpgrade(__state, text, 1);
				}
				else
				{
					SharedUpgradesPlus.LogVerbose("[Purchase] no match for '" + __state.ItemName + "', nothing to distribute.");
				}
			}
		}

		private static string? MatchItemNameToModdedUpgrade(string itemName)
		{
			string text = itemName.Replace(" ", "");
			foreach (Upgrade moddedUpgrade in RegistryService.Instance.ModdedUpgrades)
			{
				string value = moddedUpgrade.CleanName.Replace(" ", "");
				if (text.EndsWith(value, StringComparison.OrdinalIgnoreCase))
				{
					return moddedUpgrade.Name;
				}
			}
			return null;
		}
	}
	[HarmonyPatch(typeof(StatsManager), "Start")]
	internal class StatsManagerPatch
	{
		private static NetworkCallbackService? _callbackService;

		[HarmonyPostfix]
		public static void Postfix(StatsManager __instance)
		{
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			//IL_003e: Expected O, but got Unknown
			SharedUpgradesPlus.LogVerbose("StatsManager.Start: initial discovery.");
			RefreshRegistry(__instance);
			if ((Object)(object)_callbackService == (Object)null)
			{
				GameObject val = new GameObject("SharedUpgradesPlus_Services");
				_callbackService = val.AddComponent<NetworkCallbackService>();
				val.AddComponent<WatermarkService>();
				Object.DontDestroyOnLoad((Object)val);
				SharedUpgradesPlus.LogVerbose("Created NetworkCallbackService and WatermarkService.");
			}
			else
			{
				SharedUpgradesPlus.LogVerbose("NetworkCallbackService already exists, skipping.");
			}
		}

		internal static void RefreshRegistry(StatsManager statsManager)
		{
			DiscoveredUpgradesResult discoveredUpgradesResult = DiscoveryService.DiscoveredUpgrades(statsManager);
			SharedUpgradesPlus.LogVerbose($"Found {discoveredUpgradesResult.Vanilla.Count} vanilla and {discoveredUpgradesResult.Modded.Count} modded upgrade(s).");
			RegistryService.Instance.Clear();
			RegistryService.Instance.RegisterAll(discoveredUpgradesResult);
			ConfigService.LoadModsIntoConfig();
		}
	}
	[HarmonyPatch(typeof(StatsManager), "RunStartStats")]
	[HarmonyAfter(new string[] { "REPOLib" })]
	internal class StatsManagerRunStartStatsPatch
	{
		[HarmonyPostfix]
		public static void Postfix(StatsManager __instance)
		{
			SharedUpgradesPlus.LogVerbose("StatsManager.RunStartStats: re-discovering after REPOLib.");
			StatsManagerPatch.RefreshRegistry(__instance);
		}
	}
	[HarmonyPatch(typeof(StatsManager), "LoadGame")]
	internal class StatsManagerLoadGamePatch
	{
		[HarmonyPostfix]
		public static void Postfix(StatsManager __instance)
		{
			SharedUpgradesPlus.LogVerbose("StatsManager.LoadGame: re-discovering after save load.");
			StatsManagerPatch.RefreshRegistry(__instance);
		}
	}
}
namespace SharedUpgradesPlus.Models
{
	public sealed class DiscoveredUpgradesResult(HashSet<string> vanilla, HashSet<string> modded)
	{
		public HashSet<string> Vanilla { get; } = vanilla;

		public HashSet<string> Modded { get; } = modded;
	}
	public sealed class Upgrade(string Name) : IEquatable<Upgrade>
	{
		public string Name { get; } = Name;

		public string CleanName
		{
			get
			{
				if (!Name.StartsWith("playerUpgrade"))
				{
					return Name;
				}
				string name = Name;
				int length = "playerUpgrade".Length;
				return name.Substring(length, name.Length - length);
			}
		}

		public bool Equals(Upgrade? other)
		{
			if (other != null)
			{
				return other.Name == Name;
			}
			return false;
		}

		public override bool Equals(object? obj)
		{
			return Equals(obj as Upgrade);
		}

		public override int GetHashCode()
		{
			return Name.GetHashCode();
		}
	}
	public sealed class UpgradeContext(string steamID, int viewID, string playerName, Dictionary<string, int> levelsBefore, string? itemName = null)
	{
		public string SteamID { get; } = steamID;

		public int ViewID { get; } = viewID;

		public string PlayerName { get; } = playerName;

		public Dictionary<string, int> LevelsBefore { get; } = levelsBefore;

		public string? ItemName { get; } = itemName;
	}
}
namespace SharedUpgradesPlus.Configuration
{
	public enum VerbosityLevel
	{
		Off,
		Debug,
		Verbose
	}
	internal static class PluginConfig
	{
		public const string ChanceEachPlayer = "Each player";

		public const string ChanceWholeTeam = "Whole team";

		public static ConfigEntry<bool> EnableSharedUpgrades;

		public static ConfigEntry<int> SharedUpgradeChance;

		public static ConfigEntry<string> SharedUpgradeChanceMode;

		public static ConfigEntry<bool> EnableLateJoinSync;

		public static ConfigEntry<bool> EnableModdedUpgrades;

		public static ConfigEntry<bool> AvoidMoreUpgradesDoubleShare;

		public static ConfigEntry<bool> EnableSharedUpgradeHeal;

		public static ConfigEntry<bool> EnableShareNotification;

		public static ConfigEntry<VerbosityLevel> LoggingLevel;

		public static ConfigFile? ConfigFile;

		public static void Init(ConfigFile config)
		{
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Expected O, but got Unknown
			//IL_0084: Unknown result type (might be due to invalid IL or missing references)
			//IL_008e: Expected O, but got Unknown
			ConfigFile = config;
			EnableSharedUpgrades = config.Bind<bool>("General", "EnableSharedUpgrades", true, "Enable or disable all upgrade sharing");
			SharedUpgradeChance = config.Bind<int>("General", "SharedUpgradesChance", 100, new ConfigDescription("Chance per upgrade level to be shared with each player", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>()));
			SharedUpgradeChanceMode = config.Bind<string>("General", "SharedUpgradesChanceMode", "Each player", new ConfigDescription("How the share chance is rolled. 'Each player' rolls separately for every teammate, so the team spreads out. 'Whole team' rolls once per purchase, so the whole team gets the upgrade or nobody does.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[2] { "Each player", "Whole team" }), Array.Empty<object>()));
			EnableLateJoinSync = config.Bind<bool>("General", "LateJoinSync", true, "Sync upgrades to players who join mid-run");
			EnableModdedUpgrades = config.Bind<bool>("General", "EnableModdedUpgrades", true, "Sync upgrades added by other mods");
			AvoidMoreUpgradesDoubleShare = config.Bind<bool>("General", "AvoidMoreUpgradesDoubleShare", true, "When MoreUpgrades is set to team-share one of its own upgrades (its per-upgrade 'Allow Team Upgrades' or 'Sync Host Upgrades' toggles), skip sharing that upgrade here so the team doesn't get it twice");
			EnableSharedUpgradeHeal = config.Bind<bool>("Effects", "EnableSharedUpgradeHeal", false, "Heal players to full HP when receiving a shared health upgrade");
			EnableShareNotification = config.Bind<bool>("Effects", "EnableShareNotification", true, "Provide a visual effect when upgrades are shared with you");
			LoggingLevel = config.Bind<VerbosityLevel>("General", "LogLevel", VerbosityLevel.Off, "Off: key events only (sync start/result, purchases). Debug: per-player distribution results and skips. Verbose: full trace of every step.");
		}
	}
}