Decompiled source of BetterModVerifier v0.1.1

plugins/com.github.LengSword.BetterModVerifier.dll

Decompiled 3 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using Peak.Network;
using Photon.Pun;
using Photon.Realtime;
using PhotonCustomPropsUtils;
using Steamworks;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("com.github.LengSword.BetterModVerifier")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.1.0")]
[assembly: AssemblyInformationalVersion("0.1.1+4ab16c2d2e7263c8b1af4d6e1b462d67808a96c1")]
[assembly: AssemblyProduct("com.github.LengSword.BetterModVerifier")]
[assembly: AssemblyTitle("BetterModVerifier")]
[assembly: AssemblyMetadata("RepositoryUrl", "https://codeberg.org/yls-peak-mods/BetterModVerifier")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.1.1.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
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 BepInEx
{
	[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
	[Conditional("CodeGeneration")]
	[Embedded]
	internal sealed class BepInAutoPluginAttribute : Attribute
	{
		public BepInAutoPluginAttribute(string? id = null, string? name = null, string? version = null)
		{
		}
	}
}
namespace BepInEx.Preloader.Core.Patching
{
	[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
	[Conditional("CodeGeneration")]
	[Embedded]
	internal sealed class PatcherAutoPluginAttribute : Attribute
	{
		public PatcherAutoPluginAttribute(string? id = null, string? name = null, string? version = null)
		{
		}
	}
}
namespace Microsoft.CodeAnalysis
{
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace BetterModVerifier
{
	[Serializable]
	internal class ModMetadata
	{
		[JsonProperty("name")]
		public string Name { get; set; } = string.Empty;

		[JsonProperty("guid")]
		public string GUID { get; set; } = string.Empty;

		[JsonProperty("version")]
		public string Version { get; set; } = string.Empty;

		[JsonProperty("sha256")]
		public string SHA256 { get; set; } = string.Empty;
	}
	[Serializable]
	internal class IgnoredModEntry
	{
		[JsonProperty("name")]
		public string Name { get; set; } = string.Empty;

		[JsonProperty("guid")]
		public string GUID { get; set; } = string.Empty;
	}
	internal class VerificationResult
	{
		public IReadOnlyList<ModMetadata> LoadedMods { get; }

		public IReadOnlyList<ModVerificationDetail> ModDetails { get; }

		public int UnauthorizedCount { get; }

		public bool Succeeded => UnauthorizedCount == 0;

		public int MatchedRequiredModCount { get; }

		public int RequiredModCount { get; }

		public string? ErrorMessage { get; }

		public VerificationReport Report { get; }

		public VerificationResult(IReadOnlyList<ModMetadata> loadedMods, IReadOnlyList<ModVerificationDetail> modDetails, int unauthorizedCount, int matchedRequiredModCount, int requiredModCount, VerificationReport report, string? errorMessage = null)
		{
			LoadedMods = loadedMods;
			ModDetails = modDetails;
			UnauthorizedCount = unauthorizedCount;
			MatchedRequiredModCount = matchedRequiredModCount;
			RequiredModCount = requiredModCount;
			Report = report;
			ErrorMessage = errorMessage;
		}
	}
	internal enum ModVerificationStatus
	{
		Allowed,
		AllowedExtra,
		Missing,
		Unauthorized
	}
	internal class ModVerificationDetail
	{
		public ModMetadata Mod { get; }

		public ModVerificationStatus Status { get; }

		public ModVerificationDetail(ModMetadata mod, ModVerificationStatus status)
		{
			Mod = mod;
			Status = status;
		}
	}
	internal class VerificationReport
	{
		public DateTime Timestamp { get; }

		public VerificationReport(DateTime timestamp)
		{
			Timestamp = timestamp;
		}
	}
	internal static class DisplayFormat
	{
		private const string DateTimeFormat = "yyyy-MM-dd HH:mm:ss";

		internal static string FormatDateTime(DateTime value)
		{
			return value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
		}

		internal static string FormatPlayer(string playerName, int actorNumber, ulong steamId)
		{
			return $"{playerName} (#{actorNumber}) [{steamId}]";
		}

		internal static string FormatPlayer(Player player)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			return FormatPlayer(player.NickName, player.ActorNumber, ModSyncManager.GetSteamId(player).m_SteamID);
		}
	}
	internal class ModScanner
	{
		public static ModMetadata GetMetadata(string filepath, string guid, string name, string version)
		{
			return new ModMetadata
			{
				GUID = guid,
				Name = name,
				Version = version,
				SHA256 = ComputeHash(filepath)
			};
		}

		public static bool TryGetMetadata(string filepath, string guid, string name, string version, out ModMetadata metadata)
		{
			metadata = new ModMetadata();
			try
			{
				metadata = GetMetadata(filepath, guid, name, version);
				return true;
			}
			catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
			{
				return false;
			}
		}

		private static string ComputeHash(string filepath)
		{
			using FileStream inputStream = File.OpenRead(filepath);
			using SHA256 sHA = SHA256.Create();
			byte[] array = sHA.ComputeHash(inputStream);
			return BitConverter.ToString(array).Replace("-", "").ToUpperInvariant();
		}
	}
	[Serializable]
	internal sealed class RoomConfigData
	{
		public int ProtocolVersion = 4;

		public bool EnableVerifier;

		public bool ShowReport;

		public VerificationMode VerificationMode;
	}
	[Serializable]
	internal sealed class PlayerConfigData
	{
		public int ProtocolVersion = 4;

		public List<ModMetadata> Mods = new List<ModMetadata>();
	}
	[Serializable]
	internal sealed class PlayerVerificationData
	{
		public int ActorNumber;

		public ulong SteamId;

		public bool Succeeded;

		public int MatchedRequiredModCount;

		public int RequiredModCount;

		public int UnauthorizedCount;

		public string ErrorMessage = string.Empty;

		public DateTime Timestamp;
	}
	[Serializable]
	internal sealed class RoomVerificationData
	{
		public int ProtocolVersion = 4;

		public Dictionary<int, PlayerVerificationData> Players = new Dictionary<int, PlayerVerificationData>();
	}
	internal sealed class ModSyncManager : MonoBehaviourPunCallbacks
	{
		internal const int ProtocolVersion = 4;

		private const string RoomConfigKey = "BMV_ROOM_CFG";

		private const string PlayerConfigKey = "BMV_PLAYER_CFG";

		private const string RoomResultsKey = "BMV_ROOM_RESULTS";

		private const float PlayerDataTimeoutSeconds = 10f;

		private const int MaxEncodedPropertyLength = 30000;

		private PhotonScopedManager photonManager;

		private IReadOnlyList<ModMetadata> localMods = Array.Empty<ModMetadata>();

		private string verifiedModsPath = string.Empty;

		private ManualLogSource logger;

		private PlayerConnectionLog playerConnectionLog;

		private readonly Dictionary<int, PlayerVerificationData> results = new Dictionary<int, PlayerVerificationData>();

		private readonly Dictionary<int, IReadOnlyList<ModVerificationDetail>> playerModDetails = new Dictionary<int, IReadOnlyList<ModVerificationDetail>>();

		private readonly Dictionary<int, string> processedPlayerConfigs = new Dictionary<int, string>();

		private readonly HashSet<int> pendingPlayers = new HashSet<int>();

		private readonly HashSet<int> pendingKicks = new HashSet<int>();

		private readonly HashSet<int> kickedPlayers = new HashSet<int>();

		private bool initialized;

		private bool deferResultsPublish;

		internal event Action<RoomConfigData>? RoomConfigChanged;

		internal event Action<IReadOnlyCollection<PlayerVerificationData>>? VerificationChanged;

		internal event Action? JoinedRoom;

		internal void Initialize(PhotonScopedManager photonManager, IReadOnlyList<ModMetadata> localMods, string whitelistPath, ManualLogSource logger)
		{
			this.photonManager = photonManager;
			this.localMods = localMods;
			verifiedModsPath = whitelistPath;
			this.logger = logger;
			photonManager.RegisterOnJoinedRoom((Action<Player>)OnJoinedRoomFromManager);
			photonManager.RegisterRoomProperty<string>("BMV_ROOM_CFG", (RoomEventType)2, (Action<string>)OnRoomConfigReceived);
			photonManager.RegisterRoomProperty<string>("BMV_ROOM_RESULTS", (RoomEventType)1, (Action<string>)OnRoomResultsReceived);
			photonManager.RegisterPlayerProperty<string>("BMV_PLAYER_CFG", (PlayerEventType)2, (Action<Player, string>)OnPlayerConfigReceived);
			initialized = true;
		}

		private void OnDestroy()
		{
			((MonoBehaviour)this).StopAllCoroutines();
		}

		internal void ReverifyPlayers()
		{
			if (initialized && !PhotonNetwork.OfflineMode && PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient)
			{
				playerModDetails.Clear();
				processedPlayerConfigs.Clear();
				VerifyKnownPlayers();
			}
		}

		private void OnJoinedRoomFromManager(Player localPlayer)
		{
			if (!PhotonNetwork.OfflineMode)
			{
				ResetSessionState();
				this.JoinedRoom?.Invoke();
				PublishLocalPlayerData();
				if (PhotonNetwork.IsMasterClient)
				{
					PublishHostConfig();
					VerifyKnownPlayers();
				}
			}
		}

		public override void OnPlayerEnteredRoom(Player newPlayer)
		{
			if (initialized && !PhotonNetwork.OfflineMode && PhotonNetwork.IsMasterClient)
			{
				StartWaitingForPlayerData(newPlayer);
			}
		}

		public override void OnPlayerLeftRoom(Player otherPlayer)
		{
			if (initialized)
			{
				pendingPlayers.Remove(otherPlayer.ActorNumber);
				pendingKicks.Remove(otherPlayer.ActorNumber);
				kickedPlayers.Remove(otherPlayer.ActorNumber);
				processedPlayerConfigs.Remove(otherPlayer.ActorNumber);
				playerModDetails.Remove(otherPlayer.ActorNumber);
				if (PhotonNetwork.IsMasterClient && results.Remove(otherPlayer.ActorNumber))
				{
					PublishResults();
				}
			}
		}

		public override void OnMasterClientSwitched(Player newMasterClient)
		{
			if (initialized && !PhotonNetwork.OfflineMode && PhotonNetwork.IsMasterClient)
			{
				results.Clear();
				playerModDetails.Clear();
				processedPlayerConfigs.Clear();
				ClearPendingState();
				PublishHostConfig();
				VerifyKnownPlayers();
			}
		}

		private void PublishHostConfig()
		{
			RoomConfigData roomConfigData = new RoomConfigData
			{
				EnableVerifier = PluginConfig.EnableVerifier.Value,
				ShowReport = PluginConfig.ShowReport.Value,
				VerificationMode = PluginConfig.VerificationMode.Value
			};
			photonManager.SetRoomProperty("BMV_ROOM_CFG", (object)Encode(roomConfigData));
			ApplyRoomConfig(roomConfigData);
		}

		private void PublishLocalPlayerData()
		{
			try
			{
				PlayerConfigData value = new PlayerConfigData
				{
					Mods = localMods.ToList()
				};
				photonManager.SetPlayerProperty("BMV_PLAYER_CFG", (object)Encode(value));
			}
			catch (Exception ex)
			{
				logger.LogError((object)("Failed to publish local mod data: " + ex));
			}
		}

		private void OnRoomConfigReceived(string encodedConfig)
		{
			if (PhotonNetwork.OfflineMode)
			{
				return;
			}
			try
			{
				ApplyRoomConfig(Decode<RoomConfigData>(encodedConfig));
			}
			catch (Exception ex)
			{
				logger.LogWarning((object)("Unable to read host configuration: " + ex.Message));
			}
		}

		private void ApplyRoomConfig(RoomConfigData config)
		{
			if (config.ProtocolVersion == 4)
			{
				this.RoomConfigChanged?.Invoke(config);
			}
		}

		private void OnRoomResultsReceived(string encodedResults)
		{
			if (PhotonNetwork.OfflineMode)
			{
				return;
			}
			try
			{
				RoomVerificationData roomVerificationData = Decode<RoomVerificationData>(encodedResults);
				if (roomVerificationData.ProtocolVersion == 4)
				{
					this.VerificationChanged?.Invoke(roomVerificationData.Players.Values);
				}
			}
			catch (Exception ex)
			{
				logger.LogWarning((object)("Unable to read synchronized verification results: " + ex.Message));
			}
		}

		private void OnPlayerConfigReceived(Player player, string encodedData)
		{
			if (PhotonNetwork.OfflineMode || !PhotonNetwork.IsMasterClient || player == null)
			{
				return;
			}
			pendingPlayers.Remove(player.ActorNumber);
			if (!PluginConfig.EnableVerifier.Value)
			{
				return;
			}
			string text = encodedData ?? string.Empty;
			if (results.ContainsKey(player.ActorNumber) && processedPlayerConfigs.TryGetValue(player.ActorNumber, out string value) && value == text)
			{
				return;
			}
			processedPlayerConfigs[player.ActorNumber] = text;
			try
			{
				PlayerConfigData playerConfigData = Decode<PlayerConfigData>(text);
				if (playerConfigData.ProtocolVersion != 4)
				{
					throw new InvalidDataException($"Unsupported protocol version {playerConfigData.ProtocolVersion}.");
				}
				VerificationResult result = ModVerifier.VerifyPlayer(verifiedModsPath, playerConfigData.Mods ?? new List<ModMetadata>(), PluginConfig.VerificationMode.Value, logger);
				StoreResult(player, result);
			}
			catch (Exception ex)
			{
				StoreFailure(player, "Unable to read synchronized mod data: " + ex.Message);
			}
		}

		private void VerifyKnownPlayers()
		{
			if (PhotonNetwork.OfflineMode)
			{
				return;
			}
			if (!PluginConfig.EnableVerifier.Value)
			{
				results.Clear();
				playerModDetails.Clear();
				PublishResults();
				return;
			}
			Dictionary<Player, string> allPlayerProperties = photonManager.GetAllPlayerProperties<string>("BMV_PLAYER_CFG");
			deferResultsPublish = true;
			try
			{
				Player[] playerList = PhotonNetwork.PlayerList;
				foreach (Player val in playerList)
				{
					if (allPlayerProperties.TryGetValue(val, out var value))
					{
						OnPlayerConfigReceived(val, value);
					}
					else
					{
						StartWaitingForPlayerData(val);
					}
				}
			}
			finally
			{
				deferResultsPublish = false;
			}
			PublishResults();
		}

		private void ResetSessionState()
		{
			((MonoBehaviour)this).StopAllCoroutines();
			results.Clear();
			playerModDetails.Clear();
			processedPlayerConfigs.Clear();
			ClearPendingState();
			deferResultsPublish = false;
		}

		private void ClearPendingState()
		{
			pendingPlayers.Clear();
			pendingKicks.Clear();
			kickedPlayers.Clear();
		}

		private void StartWaitingForPlayerData(Player player)
		{
			if (pendingPlayers.Add(player.ActorNumber))
			{
				((MonoBehaviour)this).StartCoroutine(WaitForPlayerData(player));
			}
		}

		private IEnumerator WaitForPlayerData(Player player)
		{
			yield return (object)new WaitForSeconds(10f);
			if (PhotonNetwork.IsMasterClient && pendingPlayers.Remove(player.ActorNumber))
			{
				StoreFailure(player, "Timed out waiting for synchronized mod data.");
			}
		}

		private void StoreResult(Player player, VerificationResult result)
		{
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			playerModDetails[player.ActorNumber] = result.ModDetails.ToArray();
			PlayerVerificationData result2 = new PlayerVerificationData
			{
				ActorNumber = player.ActorNumber,
				SteamId = GetSteamId(player).m_SteamID,
				Succeeded = result.Succeeded,
				MatchedRequiredModCount = result.MatchedRequiredModCount,
				RequiredModCount = result.RequiredModCount,
				UnauthorizedCount = result.UnauthorizedCount,
				ErrorMessage = (result.ErrorMessage ?? string.Empty),
				Timestamp = result.Report.Timestamp
			};
			StoreAndPublish(player, result2);
		}

		private void StoreFailure(Player player, string error)
		{
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			playerModDetails.Remove(player.ActorNumber);
			StoreAndPublish(player, new PlayerVerificationData
			{
				ActorNumber = player.ActorNumber,
				SteamId = GetSteamId(player).m_SteamID,
				Succeeded = false,
				MatchedRequiredModCount = 0,
				RequiredModCount = 0,
				UnauthorizedCount = 0,
				ErrorMessage = error,
				Timestamp = DateTime.UtcNow
			});
		}

		internal IReadOnlyDictionary<int, IReadOnlyList<ModVerificationDetail>> GetPlayerModDetails()
		{
			return new Dictionary<int, IReadOnlyList<ModVerificationDetail>>(playerModDetails);
		}

		private void StoreAndPublish(Player player, PlayerVerificationData result)
		{
			results[player.ActorNumber] = result;
			if (!deferResultsPublish)
			{
				PublishResults();
			}
			if (!result.Succeeded && player != PhotonNetwork.LocalPlayer && PluginConfig.KickUnverifiedPlayers.Value && !kickedPlayers.Contains(player.ActorNumber) && pendingKicks.Add(player.ActorNumber))
			{
				((MonoBehaviour)this).StartCoroutine(KickWhenReady(player.ActorNumber, DisplayFormat.FormatPlayer(player)));
			}
		}

		private IEnumerator KickWhenReady(int actorNumber, string playerDisplayName)
		{
			PlayerVerificationData value;
			while (PhotonNetwork.IsMasterClient && PluginConfig.KickUnverifiedPlayers.Value && results.TryGetValue(actorNumber, out value) && !value.Succeeded)
			{
				if (NetCode.Session != null && NetCode.Session.IsHost && (Object)(object)PlayerHandler.GetPlayer(actorNumber) != (Object)null)
				{
					kickedPlayers.Add(actorNumber);
					logger.LogWarning((object)("Kicking unverified player " + playerDisplayName + "."));
					NotifyPlayer("[BetterModVerifier] Kicked unverified player: [[userColor]]" + playerDisplayName + "</color>");
					PlayerHandler.Kick(actorNumber);
					break;
				}
				yield return null;
			}
			pendingKicks.Remove(actorNumber);
		}

		private bool NotifyPlayer(string message, bool sound = true)
		{
			//IL_0052: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			//IL_008c: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)playerConnectionLog == (Object)null)
			{
				playerConnectionLog = Object.FindAnyObjectByType<PlayerConnectionLog>();
			}
			if ((Object)(object)playerConnectionLog == (Object)null)
			{
				logger.LogWarning((object)"PlayerConnectionLog not found. Cannot show notification.");
				return false;
			}
			StringBuilder stringBuilder = new StringBuilder(message);
			stringBuilder.Replace("[[userColor]]", playerConnectionLog.GetColorTag(playerConnectionLog.userColor));
			message = stringBuilder.ToString();
			playerConnectionLog.AddMessage(message);
			if (sound)
			{
				playerConnectionLog.sfxLeave.Play(default(Vector3));
			}
			return true;
		}

		private void PublishResults()
		{
			try
			{
				photonManager.SetRoomProperty("BMV_ROOM_RESULTS", (object)Encode(new RoomVerificationData
				{
					Players = new Dictionary<int, PlayerVerificationData>(results)
				}));
			}
			catch (Exception ex)
			{
				logger.LogError((object)("Failed to publish verification results: " + ex.Message));
			}
		}

		private static string Encode<T>(T value)
		{
			byte[] bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject((object)value, (Formatting)0));
			using MemoryStream memoryStream = new MemoryStream();
			using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionLevel.Optimal, leaveOpen: true))
			{
				gZipStream.Write(bytes, 0, bytes.Length);
			}
			string text = Convert.ToBase64String(memoryStream.ToArray());
			if (text.Length > 30000)
			{
				throw new InvalidDataException($"Encoded Photon property is too large ({text.Length} characters).");
			}
			return text;
		}

		private static T Decode<T>(string encoded)
		{
			if (string.IsNullOrWhiteSpace(encoded))
			{
				throw new InvalidDataException("The synchronized value is empty.");
			}
			byte[] buffer = Convert.FromBase64String(encoded);
			using MemoryStream stream = new MemoryStream(buffer);
			using GZipStream stream2 = new GZipStream(stream, CompressionMode.Decompress);
			using StreamReader streamReader = new StreamReader(stream2, Encoding.UTF8);
			T val = JsonConvert.DeserializeObject<T>(streamReader.ReadToEnd());
			T val2 = val;
			if (val2 == null)
			{
				throw new InvalidDataException("The synchronized value contains no data.");
			}
			return val2;
		}

		internal static CSteamID GetSteamId(Player player)
		{
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			if (player != null && !string.IsNullOrEmpty(player.UserId) && ulong.TryParse(player.UserId, out var result))
			{
				return new CSteamID(result);
			}
			return CSteamID.Nil;
		}
	}
	internal enum VerificationMode
	{
		ExactMatch,
		AllowExtra
	}
	internal static class ModVerifier
	{
		private const string IgnoredModsFileName = "ignored_mods.json";

		internal static VerificationResult Verify(string verifiedModsPath, IReadOnlyList<ModMetadata> loadedMods, VerificationMode mode, ManualLogSource logger)
		{
			IgnoredModEntry[] ignoredMods = LoadIgnoredMods(verifiedModsPath);
			IReadOnlyList<ModMetadata> loadedMods2 = FilterIgnoredMods(loadedMods, ignoredMods);
			EnsureWhitelistFile(verifiedModsPath, loadedMods2);
			return VerifyLoadedMods(verifiedModsPath, loadedMods2, ignoredMods, mode, logger);
		}

		internal static VerificationResult VerifyPlayer(string verifiedModsPath, IReadOnlyList<ModMetadata> loadedMods, VerificationMode mode, ManualLogSource logger)
		{
			IgnoredModEntry[] ignoredMods = LoadIgnoredMods(verifiedModsPath);
			IReadOnlyList<ModMetadata> loadedMods2 = FilterIgnoredMods(loadedMods, ignoredMods);
			return VerifyLoadedMods(verifiedModsPath, loadedMods2, ignoredMods, mode, logger);
		}

		internal static int RebuildWhitelist(string verifiedModsPath, IReadOnlyList<ModMetadata> loadedMods)
		{
			IgnoredModEntry[] ignoredMods = LoadIgnoredMods(verifiedModsPath);
			IReadOnlyList<ModMetadata> readOnlyList = FilterIgnoredMods(loadedMods, ignoredMods);
			EnsureDirectory(verifiedModsPath);
			File.WriteAllText(verifiedModsPath, JsonConvert.SerializeObject((object)readOnlyList, (Formatting)1));
			return readOnlyList.Count;
		}

		private static VerificationResult VerifyLoadedMods(string verifiedModsPath, IReadOnlyList<ModMetadata> loadedMods, IReadOnlyList<IgnoredModEntry> ignoredMods, VerificationMode mode, ManualLogSource logger)
		{
			//IL_003f: Expected O, but got Unknown
			DateTime utcNow = DateTime.UtcNow;
			List<ModVerificationDetail> list = new List<ModVerificationDetail>();
			int num = 0;
			logger.LogDebug((object)("Fetching whitelist from " + verifiedModsPath));
			ModMetadata[] array;
			try
			{
				array = FilterIgnoredMods(LoadWhitelistFile(verifiedModsPath) ?? Array.Empty<ModMetadata>(), ignoredMods).ToArray();
			}
			catch (JsonException ex)
			{
				JsonException innerException = ex;
				throw new Exception("Serializing the whitelist: malformed whitelist", (Exception?)(object)innerException);
			}
			bool[] array2 = new bool[array.Length];
			int num2 = 0;
			foreach (ModMetadata loadedMod in loadedMods)
			{
				int num3 = FindUnmatchedIndex(array, array2, loadedMod);
				if (num3 >= 0)
				{
					array2[num3] = true;
					num2++;
					list.Add(new ModVerificationDetail(loadedMod, ModVerificationStatus.Allowed));
					continue;
				}
				if (mode == VerificationMode.AllowExtra)
				{
					list.Add(new ModVerificationDetail(loadedMod, ModVerificationStatus.AllowedExtra));
					continue;
				}
				list.Add(new ModVerificationDetail(loadedMod, ModVerificationStatus.Unauthorized));
				num++;
				logger.LogWarning((object)("Unauthorized mod: " + loadedMod.Name + " (" + loadedMod.Version + ")"));
			}
			for (int i = 0; i < array.Length; i++)
			{
				if (!array2[i])
				{
					ModMetadata modMetadata = array[i];
					num++;
					list.Add(new ModVerificationDetail(modMetadata, ModVerificationStatus.Missing));
					logger.LogWarning((object)("Missing required mod: " + modMetadata.Name + " (" + modMetadata.Version + ")"));
				}
			}
			VerificationReport report = new VerificationReport(utcNow);
			return new VerificationResult(loadedMods, list, num, num2, array.Length, report);
		}

		private static bool IsMatch(ModMetadata whitelistMod, ModMetadata loadedMod)
		{
			if (string.Equals(whitelistMod.GUID, loadedMod.GUID, StringComparison.OrdinalIgnoreCase) && string.Equals(whitelistMod.Version, loadedMod.Version, StringComparison.OrdinalIgnoreCase))
			{
				return string.Equals(whitelistMod.SHA256, loadedMod.SHA256, StringComparison.OrdinalIgnoreCase);
			}
			return false;
		}

		private static int FindUnmatchedIndex(IReadOnlyList<ModMetadata> whitelist, IReadOnlyList<bool> matchedWhitelist, ModMetadata loadedMod)
		{
			for (int i = 0; i < whitelist.Count; i++)
			{
				if (!matchedWhitelist[i] && IsMatch(whitelist[i], loadedMod))
				{
					return i;
				}
			}
			return -1;
		}

		private static void EnsureWhitelistFile(string verifiedModsPath, IReadOnlyList<ModMetadata> loadedMods)
		{
			if (!File.Exists(verifiedModsPath))
			{
				EnsureDirectory(verifiedModsPath);
				File.WriteAllText(verifiedModsPath, JsonConvert.SerializeObject((object)loadedMods, (Formatting)1));
			}
		}

		private static ModMetadata[]? LoadWhitelistFile(string filePath)
		{
			return JsonConvert.DeserializeObject<ModMetadata[]>(File.ReadAllText(filePath));
		}

		private static IgnoredModEntry[] LoadIgnoredMods(string verifiedModsPath)
		{
			//IL_0045: Expected O, but got Unknown
			string ignoredModsPath = GetIgnoredModsPath(verifiedModsPath);
			if (!File.Exists(ignoredModsPath))
			{
				EnsureDirectory(ignoredModsPath);
				File.WriteAllText(ignoredModsPath, JsonConvert.SerializeObject((object)Array.Empty<IgnoredModEntry>(), (Formatting)1));
				return Array.Empty<IgnoredModEntry>();
			}
			try
			{
				return JsonConvert.DeserializeObject<IgnoredModEntry[]>(File.ReadAllText(ignoredModsPath)) ?? Array.Empty<IgnoredModEntry>();
			}
			catch (JsonException ex)
			{
				JsonException innerException = ex;
				throw new Exception("Serializing the ignored mods: malformed ignored mod list", (Exception?)(object)innerException);
			}
		}

		private static IReadOnlyList<ModMetadata> FilterIgnoredMods(IReadOnlyList<ModMetadata> mods, IReadOnlyList<IgnoredModEntry> ignoredMods)
		{
			if (ignoredMods.Count == 0)
			{
				return mods;
			}
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (IgnoredModEntry ignoredMod in ignoredMods)
			{
				if (!string.IsNullOrWhiteSpace(ignoredMod.GUID))
				{
					hashSet.Add(ignoredMod.GUID);
				}
			}
			if (hashSet.Count == 0)
			{
				return mods;
			}
			List<ModMetadata> list = new List<ModMetadata>();
			foreach (ModMetadata mod in mods)
			{
				if (!hashSet.Contains(mod.GUID))
				{
					list.Add(mod);
				}
			}
			return list;
		}

		private static string GetIgnoredModsPath(string verifiedModsPath)
		{
			string directoryName = Path.GetDirectoryName(verifiedModsPath);
			return Path.Combine(directoryName ?? string.Empty, "ignored_mods.json");
		}

		private static void EnsureDirectory(string filePath)
		{
			string directoryName = Path.GetDirectoryName(filePath);
			if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
			{
				Directory.CreateDirectory(directoryName);
			}
		}
	}
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInPlugin("com.github.LengSword.BetterModVerifier", "BetterModVerifier", "0.1.1")]
	public class Plugin : BaseUnityPlugin
	{
		private static readonly string DataDirectory = Path.Combine(Application.persistentDataPath, "BetterModVerifier");

		private static readonly string VerifiedModsPath = Path.Combine(DataDirectory, "verified_mods.json");

		private static readonly string LogPath = Path.Combine(DataDirectory, "verifier_result.log");

		private GameObject managerObject;

		private VerifierReportManager reportManager;

		private ModSyncManager modSyncManager;

		private IReadOnlyList<ModMetadata> localMods = Array.Empty<ModMetadata>();

		private bool runStarted;

		private bool hostShowReport = true;

		private bool lastOfflineMode;

		private DateTime lastLoggedNetworkResultTimestamp;

		private VerificationResult? localVerificationResult;

		public const string Id = "com.github.LengSword.BetterModVerifier";

		internal static ManualLogSource Logger { get; private set; } = null;

		public static string Name => "BetterModVerifier";

		public static string Version => "0.1.1";

		private void Awake()
		{
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			Logger = ((BaseUnityPlugin)this).Logger;
			Logger.LogInfo((object)("Plugin " + Name + " is loaded!"));
			PluginConfig.Initialize(((BaseUnityPlugin)this).Config, Logger);
			CreateManagers();
			PluginConfig.ShowReport.SettingChanged += OnShowReportChanged;
			GameRunState.Changed += OnRunStateChanged;
			new Harmony("com.github.LengSword.BetterModVerifier").PatchAll();
		}

		private void Start()
		{
			localMods = GetLoadedMods();
			Logger.LogInfo((object)$"Found {localMods.Count} loaded plugins.");
			SetupNetworkSync();
		}

		private void Update()
		{
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			bool offlineMode = PhotonNetwork.OfflineMode;
			if (offlineMode != lastOfflineMode)
			{
				lastOfflineMode = offlineMode;
				RefreshReportVisibility();
			}
			if (PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient && Input.GetKeyDown(PluginConfig.RebuildWhitelistKey.Value))
			{
				RebuildWhitelist();
			}
		}

		private void OnDestroy()
		{
			ConfigEntry<bool> showReport = PluginConfig.ShowReport;
			if (showReport != null)
			{
				showReport.SettingChanged -= OnShowReportChanged;
			}
			GameRunState.Changed -= OnRunStateChanged;
			if ((Object)(object)modSyncManager != (Object)null)
			{
				modSyncManager.RoomConfigChanged -= OnRoomConfigChanged;
				modSyncManager.VerificationChanged -= OnNetworkVerificationChanged;
				modSyncManager.JoinedRoom -= OnJoinedRoom;
			}
			if ((Object)(object)managerObject != (Object)null)
			{
				Object.Destroy((Object)(object)managerObject);
			}
		}

		private void CreateManagers()
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Expected O, but got Unknown
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			managerObject = new GameObject("BetterModVerifierManagers");
			Object.DontDestroyOnLoad((Object)(object)managerObject);
			reportManager = managerObject.AddComponent<VerifierReportManager>();
			reportManager.Configure(PluginConfig.ReportPos.Value, new Vector2(PluginConfig.ReportPaddingLeft.Value, PluginConfig.ReportPaddingBottom.Value));
			lastOfflineMode = PhotonNetwork.OfflineMode;
			RefreshReportVisibility();
			modSyncManager = managerObject.AddComponent<ModSyncManager>();
		}

		private void OnShowReportChanged(object sender, EventArgs eventArgs)
		{
			RefreshReportVisibility();
		}

		private void SetupNetworkSync()
		{
			PhotonScopedManager manager = PhotonCustomPropsUtilsPlugin.GetManager("com.github.LengSword.BetterModVerifier");
			modSyncManager.RoomConfigChanged += OnRoomConfigChanged;
			modSyncManager.VerificationChanged += OnNetworkVerificationChanged;
			modSyncManager.JoinedRoom += OnJoinedRoom;
			modSyncManager.Initialize(manager, localMods, VerifiedModsPath, Logger);
		}

		private void OnJoinedRoom()
		{
			RunLocalVerification();
		}

		private void RunLocalVerification()
		{
			if (PhotonNetwork.OfflineMode)
			{
				Logger.LogInfo((object)"Verification is disabled in Photon offline mode.");
				return;
			}
			if (!PluginConfig.EnableVerifier.Value)
			{
				Logger.LogInfo((object)"Verification is disabled.");
				return;
			}
			Logger.LogInfo((object)"Starting verification...");
			try
			{
				VerificationResult result = ModVerifier.Verify(VerifiedModsPath, localMods, PluginConfig.VerificationMode.Value, Logger);
				WriteAndDisplayResult(result);
			}
			catch (Exception ex)
			{
				Logger.LogError((object)("Verification failed: " + ex));
				reportManager.SetReportError(ex.Message);
			}
		}

		private void RebuildWhitelist()
		{
			try
			{
				int num = ModVerifier.RebuildWhitelist(VerifiedModsPath, localMods);
				Logger.LogInfo((object)$"Rebuilt whitelist with {num} verified plugins: {VerifiedModsPath}");
				RunLocalVerification();
				modSyncManager.ReverifyPlayers();
			}
			catch (Exception ex)
			{
				Logger.LogError((object)("Failed to rebuild whitelist: " + ex));
				reportManager.SetReportError("Failed to rebuild whitelist: " + ex.Message);
			}
		}

		private void OnRoomConfigChanged(RoomConfigData config)
		{
			hostShowReport = config.ShowReport;
			RefreshReportVisibility();
			Logger.LogDebug((object)($"Received host config: verifier={config.EnableVerifier}, " + $"mode={config.VerificationMode}, showReport={config.ShowReport}."));
		}

		private void OnRunStateChanged(bool started)
		{
			runStarted = started;
			RefreshReportVisibility();
		}

		private void RefreshReportVisibility()
		{
			bool visible = !PhotonNetwork.OfflineMode && ((PhotonNetwork.InRoom && !PhotonNetwork.IsMasterClient) ? (hostShowReport && !runStarted) : PluginConfig.ShowReport.Value);
			reportManager.SetVisible(visible);
		}

		private void OnNetworkVerificationChanged(IReadOnlyCollection<PlayerVerificationData> results)
		{
			if (PhotonNetwork.OfflineMode || !PhotonNetwork.InRoom)
			{
				return;
			}
			PlayerVerificationData playerVerificationData = null;
			int num = 0;
			int num2 = 0;
			List<string> list = new List<string>();
			foreach (PlayerVerificationData result in results)
			{
				if (result.ActorNumber == PhotonNetwork.LocalPlayer.ActorNumber)
				{
					playerVerificationData = result;
				}
				else if (PhotonNetwork.IsMasterClient)
				{
					num2++;
					if (result.Succeeded)
					{
						num++;
					}
					else
					{
						list.Add(GetPlayerDisplayName(result));
					}
				}
			}
			if (PhotonNetwork.IsMasterClient)
			{
				list.Sort(StringComparer.OrdinalIgnoreCase);
				reportManager.SetClientSummary(num, num2, list);
			}
			else
			{
				reportManager.ClearClientSummary();
			}
			if (playerVerificationData == null)
			{
				return;
			}
			if (!string.IsNullOrEmpty(playerVerificationData.ErrorMessage))
			{
				reportManager.SetReportError(playerVerificationData.ErrorMessage);
				return;
			}
			if (playerVerificationData.Timestamp != lastLoggedNetworkResultTimestamp)
			{
				lastLoggedNetworkResultTimestamp = playerVerificationData.Timestamp;
				Logger.LogInfo((object)("Network verification finished with " + (playerVerificationData.Succeeded ? "success" : "fail") + " at " + DisplayFormat.FormatDateTime(playerVerificationData.Timestamp)));
			}
			if (localVerificationResult != null)
			{
				WriteReportFile(localVerificationResult, results, modSyncManager.GetPlayerModDetails());
			}
			reportManager.SetSummary(playerVerificationData.Timestamp, playerVerificationData.Succeeded, playerVerificationData.MatchedRequiredModCount, playerVerificationData.RequiredModCount);
		}

		private static string GetPlayerDisplayName(PlayerVerificationData result)
		{
			Player[] playerList = PhotonNetwork.PlayerList;
			foreach (Player val in playerList)
			{
				if (val.ActorNumber == result.ActorNumber)
				{
					return DisplayFormat.FormatPlayer(val);
				}
			}
			return DisplayFormat.FormatPlayer(string.Empty, result.ActorNumber, result.SteamId);
		}

		private static string GetLocalPlayerDisplayName()
		{
			if (PhotonNetwork.LocalPlayer == null)
			{
				return DisplayFormat.FormatPlayer(string.Empty, 0, 0uL);
			}
			return DisplayFormat.FormatPlayer(PhotonNetwork.LocalPlayer);
		}

		private void WriteAndDisplayResult(VerificationResult result)
		{
			localVerificationResult = result;
			Logger.LogInfo((object)("Verification finished with " + (result.Succeeded ? "success" : "fail") + " at " + DisplayFormat.FormatDateTime(result.Report.Timestamp)));
			WriteReportFile(result, null);
			reportManager.SetResult(result);
		}

		private void WriteReportFile(VerificationResult localResult, IReadOnlyCollection<PlayerVerificationData>? networkResults, IReadOnlyDictionary<int, IReadOnlyList<ModVerificationDetail>>? playerModDetails = null)
		{
			Logger.LogInfo((object)("Writing report into " + LogPath));
			File.WriteAllText(LogPath, BuildReportText(localResult, networkResults, playerModDetails));
		}

		private static string BuildReportText(VerificationResult localResult, IReadOnlyCollection<PlayerVerificationData>? networkResults, IReadOnlyDictionary<int, IReadOnlyList<ModVerificationDetail>>? playerModDetails)
		{
			using StringWriter stringWriter = new StringWriter();
			stringWriter.WriteLine("=== Verification Report (" + Name + " v" + Version + ") ===");
			stringWriter.WriteLine("Timestamp: " + DisplayFormat.FormatDateTime(localResult.Report.Timestamp));
			stringWriter.WriteLine("Whitelist source: " + VerifiedModsPath);
			stringWriter.WriteLine($"Verification mode: {PluginConfig.VerificationMode.Value}");
			if (networkResults == null || networkResults.Count == 0)
			{
				stringWriter.WriteLine();
				WritePlayerReportSection(stringWriter, "Host", GetLocalPlayerDisplayName(), localResult.Succeeded, localResult.Report.Timestamp, localResult.MatchedRequiredModCount, localResult.RequiredModCount, localResult.UnauthorizedCount, localResult.ErrorMessage, localResult.ModDetails);
				return stringWriter.ToString();
			}
			List<PlayerVerificationData> list = new List<PlayerVerificationData>(networkResults);
			list.Sort((PlayerVerificationData left, PlayerVerificationData right) => left.ActorNumber.CompareTo(right.ActorNumber));
			int num = 0;
			foreach (PlayerVerificationData item in list)
			{
				if (item.Succeeded)
				{
					num++;
				}
			}
			stringWriter.WriteLine();
			stringWriter.WriteLine("=== Network Verification Results ===");
			stringWriter.WriteLine($"Players: {num}/{list.Count} Passed");
			foreach (PlayerVerificationData item2 in list)
			{
				string role = (IsHostPlayer(item2.ActorNumber) ? "Host" : "Client");
				IReadOnlyList<ModVerificationDetail> value = null;
				playerModDetails?.TryGetValue(item2.ActorNumber, out value);
				WritePlayerReportSection(stringWriter, role, GetPlayerDisplayName(item2), item2.Succeeded, item2.Timestamp, item2.MatchedRequiredModCount, item2.RequiredModCount, item2.UnauthorizedCount, item2.ErrorMessage, value);
			}
			return stringWriter.ToString();
		}

		private static void WritePlayerReportSection(StringWriter writer, string role, string playerDisplayName, bool succeeded, DateTime timestamp, int matchedRequiredModCount, int requiredModCount, int unauthorizedCount, string? errorMessage, IReadOnlyList<ModVerificationDetail>? modDetails)
		{
			writer.WriteLine();
			writer.WriteLine("[" + role + "] " + playerDisplayName);
			writer.WriteLine("Status: " + (succeeded ? "Passed" : "Failed"));
			writer.WriteLine("Verification Time: " + DisplayFormat.FormatDateTime(timestamp));
			writer.WriteLine($"Required Mods: {matchedRequiredModCount}/{requiredModCount}");
			writer.WriteLine($"Unauthorized Mods: {unauthorizedCount}");
			if (!string.IsNullOrEmpty(errorMessage))
			{
				writer.WriteLine("Error: " + errorMessage);
			}
			if (modDetails == null || modDetails.Count == 0)
			{
				return;
			}
			writer.WriteLine("Mods:");
			foreach (ModVerificationDetail modDetail in modDetails)
			{
				writer.WriteLine("- " + modDetail.Mod.Name + " (" + modDetail.Mod.GUID + ") v" + modDetail.Mod.Version);
				writer.WriteLine("    SHA256: " + modDetail.Mod.SHA256);
				writer.WriteLine("    Status: " + FormatModStatus(modDetail.Status));
			}
		}

		private static string FormatModStatus(ModVerificationStatus status)
		{
			return status switch
			{
				ModVerificationStatus.Allowed => "Allowed", 
				ModVerificationStatus.AllowedExtra => "Allowed (Extra)", 
				ModVerificationStatus.Missing => "Missing", 
				ModVerificationStatus.Unauthorized => "Unauthorized", 
				_ => status.ToString(), 
			};
		}

		private static bool IsHostPlayer(int actorNumber)
		{
			if (PhotonNetwork.MasterClient != null)
			{
				return PhotonNetwork.MasterClient.ActorNumber == actorNumber;
			}
			return false;
		}

		private List<ModMetadata> GetLoadedMods()
		{
			List<ModMetadata> list = new List<ModMetadata>();
			foreach (PluginInfo value in Chainloader.PluginInfos.Values)
			{
				string location = value.Location;
				if ((string.IsNullOrEmpty(location) || !File.Exists(location)) && (Object)(object)value.Instance != (Object)null)
				{
					location = ((object)value.Instance).GetType().Assembly.Location;
				}
				ModMetadata metadata;
				if (string.IsNullOrEmpty(location) || !File.Exists(location))
				{
					Logger.LogWarning((object)("Unable to locate plugin file for " + value.Metadata.GUID + "."));
				}
				else if (!ModScanner.TryGetMetadata(location, value.Metadata.GUID, value.Metadata.Name, value.Metadata.Version.ToString(), out metadata))
				{
					Logger.LogWarning((object)("Unable to retrieve metadata from " + location));
				}
				else
				{
					list.Add(metadata);
				}
			}
			return list;
		}
	}
	internal static class GameRunState
	{
		internal static event Action<bool>? Changed;

		internal static void SetStarted(bool started)
		{
			GameRunState.Changed?.Invoke(started);
		}
	}
	[HarmonyPatch(typeof(RunManager), "StartRun")]
	internal static class RunManagerStartRunPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			GameRunState.SetStarted(started: true);
		}
	}
	[HarmonyPatch(typeof(RunManager), "EndGame")]
	internal static class RunManagerEndGamePatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			GameRunState.SetStarted(started: false);
		}
	}
	[HarmonyPatch(typeof(Player), "LeaveCurrentGame")]
	internal static class PlayerLeaveCurrentGamePatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			GameRunState.SetStarted(started: false);
		}
	}
	public static class PluginConfig
	{
		internal static ConfigEntry<bool> EnableVerifier { get; private set; }

		internal static ConfigEntry<bool> ShowReport { get; private set; }

		internal static ConfigEntry<bool> KickUnverifiedPlayers { get; private set; }

		internal static ConfigEntry<VerificationMode> VerificationMode { get; private set; }

		internal static ConfigEntry<ReportPosition> ReportPos { get; private set; }

		internal static ConfigEntry<float> ReportPaddingLeft { get; private set; }

		internal static ConfigEntry<float> ReportPaddingBottom { get; private set; }

		internal static ConfigEntry<KeyCode> RebuildWhitelistKey { get; private set; }

		public static void Initialize(ConfigFile config, ManualLogSource logger)
		{
			EnableVerifier = config.Bind<bool>("General", "EnableVerifier", true, "Check if players joining your room have the same mods installed.");
			ShowReport = config.Bind<bool>("General", "ShowReport", true, "Show the verification results.");
			KickUnverifiedPlayers = config.Bind<bool>("General", "KickUnverifiedPlayers", false, "Kick players who fail the verification.");
			VerificationMode = config.Bind<VerificationMode>("General", "VerificationMode", BetterModVerifier.VerificationMode.ExactMatch, "ExactMatch requires the player's mod set to match the whitelist. AllowExtra requires all whitelist mods but permits additional player mods.");
			ReportPos = config.Bind<ReportPosition>("UI", "ReportPosition", ReportPosition.Right, "The position of the verification report on the screen.");
			ReportPaddingLeft = config.Bind<float>("UI", "ReportPaddingLeft", -20f, "The left padding of the verification report. Set to negative values to move left instead.");
			ReportPaddingBottom = config.Bind<float>("UI", "ReportPaddingBottom", 0f, "The bottom padding of the verification report. Set to negative values to move down instead.");
			RebuildWhitelistKey = config.Bind<KeyCode>("KeyBinds", "RebuildWhitelistKey", (KeyCode)285, "The key to rebuild the host's local whitelist file.");
			logger.LogInfo((object)"Plugin Config Loaded.");
		}
	}
	internal enum ReportPosition
	{
		Top,
		Bottom,
		Left,
		TopLeft,
		BottomLeft,
		Right,
		TopRight,
		BottomRight
	}
	internal class VerifierReportManager : MonoBehaviour
	{
		private TextMeshProUGUI report_textblock;

		private GameObject reportPanel;

		private DateTime timestamp;

		private bool hasSummary;

		private bool succeeded;

		private int matchedRequiredModCount;

		private int requiredModCount;

		private string errorMessage = string.Empty;

		private bool showClientSummary;

		private int passedClientCount;

		private int clientCount;

		private readonly List<string> failedPlayerNames = new List<string>();

		private TMP_FontAsset? currentFont;

		private bool requestedVisible;

		public void Configure(ReportPosition position, Vector2 padding)
		{
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_0057: Unknown result type (might be due to invalid IL or missing references)
			//IL_0061: Expected O, but got Unknown
			//IL_0095: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c9: Expected O, but got Unknown
			//IL_010f: Unknown result type (might be due to invalid IL or missing references)
			timestamp = DateTime.UtcNow;
			Canvas val = ((Component)this).gameObject.AddComponent<Canvas>();
			val.renderMode = (RenderMode)0;
			val.sortingOrder = 1000;
			CanvasScaler val2 = ((Component)this).gameObject.AddComponent<CanvasScaler>();
			val2.uiScaleMode = (ScaleMode)1;
			val2.referenceResolution = new Vector2(1920f, 1080f);
			reportPanel = new GameObject("VerifierPanel");
			reportPanel.transform.SetParent(((Component)this).gameObject.transform, false);
			reportPanel.AddComponent<RectTransform>();
			PlaceInPosition(reportPanel.GetComponent<RectTransform>(), position, padding);
			VerticalLayoutGroup val3 = reportPanel.AddComponent<VerticalLayoutGroup>();
			((HorizontalOrVerticalLayoutGroup)val3).spacing = 10f;
			((LayoutGroup)val3).childAlignment = PositionToTextAnchor(position);
			GameObject val4 = new GameObject("VerifierReport");
			val4.transform.SetParent(reportPanel.transform, false);
			report_textblock = val4.AddComponent<TextMeshProUGUI>();
			((TMP_Text)report_textblock).text = "Loading report...";
			((TMP_Text)report_textblock).richText = false;
			((TMP_Text)report_textblock).alignment = PositionToAlignment(position);
			reportPanel.SetActive(false);
		}

		public void SetResult(VerificationResult result)
		{
			SetSummary(result.Report.Timestamp, result.Succeeded, result.MatchedRequiredModCount, result.RequiredModCount);
		}

		public void SetSummary(DateTime verificationTime, bool succeeded, int matchedRequiredModCount, int requiredModCount)
		{
			timestamp = verificationTime;
			this.succeeded = succeeded;
			this.matchedRequiredModCount = matchedRequiredModCount;
			this.requiredModCount = requiredModCount;
			errorMessage = string.Empty;
			hasSummary = true;
			UpdateReportText();
		}

		public void SetReportError(string error)
		{
			errorMessage = error;
			hasSummary = false;
			UpdateReportText();
		}

		public void SetClientSummary(int passedClientCount, int clientCount, IReadOnlyCollection<string> failedPlayers)
		{
			this.passedClientCount = passedClientCount;
			this.clientCount = clientCount;
			failedPlayerNames.Clear();
			failedPlayerNames.AddRange(failedPlayers);
			showClientSummary = true;
			UpdateReportText();
		}

		public void ClearClientSummary()
		{
			if (showClientSummary)
			{
				showClientSummary = false;
				failedPlayerNames.Clear();
				UpdateReportText();
			}
		}

		public void SetVisible(bool visible)
		{
			requestedVisible = visible;
			ApplyVisibility();
		}

		private void Update()
		{
			ApplyVisibility();
		}

		private void UpdateReportText()
		{
			string text = (hasSummary ? ("Verification Time: " + DisplayFormat.FormatDateTime(timestamp) + "\r\n" + $"Required Mods: {matchedRequiredModCount}/{requiredModCount}\r\n" + "Status: " + (succeeded ? "Passed" : "Failed")) : (string.IsNullOrEmpty(errorMessage) ? "Loading report..." : ("Error during verification:\r\n" + errorMessage)));
			if (showClientSummary)
			{
				text += $"\r\n\r\nClients: {passedClientCount}/{clientCount} Passed";
				if (failedPlayerNames.Count > 0)
				{
					text += "\r\nFailed Players:";
					foreach (string failedPlayerName in failedPlayerNames)
					{
						text = text + "\r\n- [Failed] " + failedPlayerName;
					}
				}
				else
				{
					text += "\r\nFailed Players: None";
				}
			}
			((TMP_Text)report_textblock).text = text;
		}

		public static TextAnchor PositionToTextAnchor(ReportPosition position)
		{
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			return (TextAnchor)(position switch
			{
				ReportPosition.Top => 1, 
				ReportPosition.Bottom => 7, 
				ReportPosition.Left => 3, 
				ReportPosition.TopLeft => 0, 
				ReportPosition.BottomLeft => 6, 
				ReportPosition.Right => 5, 
				ReportPosition.TopRight => 2, 
				ReportPosition.BottomRight => 8, 
				_ => 3, 
			});
		}

		public static TextAlignmentOptions PositionToAlignment(ReportPosition position)
		{
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			switch (position)
			{
			case ReportPosition.Top:
			case ReportPosition.Bottom:
				return (TextAlignmentOptions)514;
			case ReportPosition.Left:
			case ReportPosition.TopLeft:
			case ReportPosition.BottomLeft:
				return (TextAlignmentOptions)513;
			case ReportPosition.Right:
			case ReportPosition.TopRight:
			case ReportPosition.BottomRight:
				return (TextAlignmentOptions)516;
			default:
				return (TextAlignmentOptions)513;
			}
		}

		private static void SetFont(TextMeshProUGUI textMesh, TMP_FontAsset font)
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Unknown result type (might be due to invalid IL or missing references)
			((TMP_Text)textMesh).font = font;
			((TMP_Text)textMesh).fontSize = 20f;
			((TMP_Text)textMesh).overflowMode = (TextOverflowModes)0;
			((TMP_Text)textMesh).textWrappingMode = (TextWrappingModes)0;
			((Graphic)textMesh).color = Color.white;
			((TMP_Text)textMesh).outlineWidth = 0.075f;
			((TMP_Text)textMesh).outlineColor = Color32.op_Implicit(Color.black);
		}

		private void ApplyVisibility()
		{
			reportPanel.SetActive(requestedVisible && TryApplyGameFont());
		}

		private bool TryApplyGameFont()
		{
			GUIManager instance = GUIManager.instance;
			if ((Object)(object)instance == (Object)null || (Object)(object)instance.heroDayText == (Object)null || (Object)(object)((TMP_Text)instance.heroDayText).font == (Object)null)
			{
				return false;
			}
			TMP_FontAsset font = ((TMP_Text)instance.heroDayText).font;
			if ((Object)(object)currentFont != (Object)(object)font)
			{
				currentFont = font;
				SetFont(report_textblock, font);
			}
			return true;
		}

		private static void PlaceInPosition(RectTransform rect, ReportPosition pos, Vector2 padding)
		{
			//IL_004e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0096: Unknown result type (might be due to invalid IL or missing references)
			//IL_009b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0084: Unknown result type (might be due to invalid IL or missing references)
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			//IL_0060: Unknown result type (might be due to invalid IL or missing references)
			//IL_0065: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ba: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bf: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00dd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00df: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00eb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d1: Unknown result type (might be due to invalid IL or missing references)
			Vector2 val = (rect.anchorMin = (Vector2)(pos switch
			{
				ReportPosition.TopLeft => new Vector2(0f, 1f), 
				ReportPosition.Top => new Vector2(0.5f, 1f), 
				ReportPosition.TopRight => new Vector2(1f, 1f), 
				ReportPosition.Left => new Vector2(0f, 0.5f), 
				ReportPosition.Right => new Vector2(1f, 0.5f), 
				ReportPosition.BottomLeft => new Vector2(0f, 0f), 
				ReportPosition.Bottom => new Vector2(0.5f, 0f), 
				ReportPosition.BottomRight => new Vector2(1f, 0f), 
				_ => new Vector2(0f, 1f), 
			}));
			Vector2 val3 = (rect.anchorMax = val);
			Vector2 pivot = val3;
			rect.pivot = pivot;
			rect.anchoredPosition = padding;
		}
	}
}
namespace System.Diagnostics.CodeAnalysis
{
	[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class ConstantExpectedAttribute : Attribute
	{
		public object? Min { get; set; }

		public object? Max { get; set; }
	}
	[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class ExperimentalAttribute : Attribute
	{
		public string DiagnosticId { get; }

		public string? UrlFormat { get; set; }

		public ExperimentalAttribute(string diagnosticId)
		{
			DiagnosticId = diagnosticId;
		}
	}
	[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
	[ExcludeFromCodeCoverage]
	internal sealed class MemberNotNullAttribute : Attribute
	{
		public string[] Members { get; }

		public MemberNotNullAttribute(string member)
		{
			Members = new string[1] { member };
		}

		public MemberNotNullAttribute(params string[] members)
		{
			Members = members;
		}
	}
	[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
	[ExcludeFromCodeCoverage]
	internal sealed class MemberNotNullWhenAttribute : Attribute
	{
		public bool ReturnValue { get; }

		public string[] Members { get; }

		public MemberNotNullWhenAttribute(bool returnValue, string member)
		{
			ReturnValue = returnValue;
			Members = new string[1] { member };
		}

		public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
		{
			ReturnValue = returnValue;
			Members = members;
		}
	}
	[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class SetsRequiredMembersAttribute : Attribute
	{
	}
	[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class StringSyntaxAttribute : Attribute
	{
		public const string CompositeFormat = "CompositeFormat";

		public const string DateOnlyFormat = "DateOnlyFormat";

		public const string DateTimeFormat = "DateTimeFormat";

		public const string EnumFormat = "EnumFormat";

		public const string GuidFormat = "GuidFormat";

		public const string Json = "Json";

		public const string NumericFormat = "NumericFormat";

		public const string Regex = "Regex";

		public const string TimeOnlyFormat = "TimeOnlyFormat";

		public const string TimeSpanFormat = "TimeSpanFormat";

		public const string Uri = "Uri";

		public const string Xml = "Xml";

		public string Syntax { get; }

		public object?[] Arguments { get; }

		public StringSyntaxAttribute(string syntax)
		{
			Syntax = syntax;
			Arguments = new object[0];
		}

		public StringSyntaxAttribute(string syntax, params object?[] arguments)
		{
			Syntax = syntax;
			Arguments = arguments;
		}
	}
	[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class UnscopedRefAttribute : Attribute
	{
	}
}
namespace System.Runtime.Versioning
{
	[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class RequiresPreviewFeaturesAttribute : Attribute
	{
		public string? Message { get; }

		public string? Url { get; set; }

		public RequiresPreviewFeaturesAttribute()
		{
		}

		public RequiresPreviewFeaturesAttribute(string? message)
		{
			Message = message;
		}
	}
}
namespace System.Runtime.CompilerServices
{
	[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
	internal sealed class IgnoresAccessChecksToAttribute : Attribute
	{
		public IgnoresAccessChecksToAttribute(string assemblyName)
		{
		}
	}
	[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class CallerArgumentExpressionAttribute : Attribute
	{
		public string ParameterName { get; }

		public CallerArgumentExpressionAttribute(string parameterName)
		{
			ParameterName = parameterName;
		}
	}
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class CollectionBuilderAttribute : Attribute
	{
		public Type BuilderType { get; }

		public string MethodName { get; }

		public CollectionBuilderAttribute(Type builderType, string methodName)
		{
			BuilderType = builderType;
			MethodName = methodName;
		}
	}
	[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class CompilerFeatureRequiredAttribute : Attribute
	{
		public const string RefStructs = "RefStructs";

		public const string RequiredMembers = "RequiredMembers";

		public string FeatureName { get; }

		public bool IsOptional { get; set; }

		public CompilerFeatureRequiredAttribute(string featureName)
		{
			FeatureName = featureName;
		}
	}
	[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class InterpolatedStringHandlerArgumentAttribute : Attribute
	{
		public string[] Arguments { get; }

		public InterpolatedStringHandlerArgumentAttribute(string argument)
		{
			Arguments = new string[1] { argument };
		}

		public InterpolatedStringHandlerArgumentAttribute(params string[] arguments)
		{
			Arguments = arguments;
		}
	}
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class InterpolatedStringHandlerAttribute : Attribute
	{
	}
	[EditorBrowsable(EditorBrowsableState.Never)]
	[ExcludeFromCodeCoverage]
	internal static class IsExternalInit
	{
	}
	[AttributeUsage(AttributeTargets.Method, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class ModuleInitializerAttribute : Attribute
	{
	}
	[AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class OverloadResolutionPriorityAttribute : Attribute
	{
		public int Priority { get; }

		public OverloadResolutionPriorityAttribute(int priority)
		{
			Priority = priority;
		}
	}
	[AttributeUsage(AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class ParamCollectionAttribute : Attribute
	{
	}
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class RequiredMemberAttribute : Attribute
	{
	}
	[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
	[EditorBrowsable(EditorBrowsableState.Never)]
	[ExcludeFromCodeCoverage]
	internal sealed class RequiresLocationAttribute : Attribute
	{
	}
	[AttributeUsage(AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Interface, Inherited = false)]
	[ExcludeFromCodeCoverage]
	internal sealed class SkipLocalsInitAttribute : Attribute
	{
	}
}