Decompiled source of ModSentry v1.0.20

Landoria.ModSentry.dll

Decompiled 21 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Logging;
using HarmonyLib;
using Mono.Cecil;
using TMPro;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("Landoria.ModSentry")]
[assembly: AssemblyDescription("Strict client mod inventory validation for Valheim servers")]
[assembly: AssemblyCompany("Landoria Gaming")]
[assembly: AssemblyProduct("Landoria.ModSentry")]
[assembly: AssemblyCopyright("Copyright © Landoria Gaming 2026")]
[assembly: ComVisible(false)]
[assembly: Guid("dc95b791-79c8-4c9c-9d44-e33e87ad806e")]
[assembly: AssemblyFileVersion("1.0.20")]
[assembly: AssemblyInformationalVersion("1.0.20")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyVersion("1.0.20.11938")]
namespace Landoria.ModSentry;

internal static class ClientMessage
{
	internal static void Receive(ZRpc rpc, string message)
	{
		ConnectionFailureMessages.Push("Landoria.ModSentry", message);
		ModSentryPlugin.Log.LogWarning((object)("Server rejected the connection: " + message));
		rpc.Invoke("Landoria_ModSentry_RejectionAck", Array.Empty<object>());
		ModSentryPlugin.Log.LogDebug((object)"Acknowledged the rejection; waiting for the server disconnect.");
	}

	internal static void Clear()
	{
		ConnectionFailureMessages.Clear("Landoria.ModSentry");
	}
}
[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
internal static class RegisterHandshakePatch
{
	private static void Postfix(ZNet __instance, ZNetPeer peer)
	{
		ModSentryHandshake.Register(__instance, peer);
	}
}
[HarmonyPatch(typeof(ZNet), "SendPeerInfo")]
[HarmonyBefore(new string[] { "Landoria.CharacterVault" })]
internal static class SendInventoryPatch
{
	private static bool Prefix(ZNet __instance, ZRpc rpc, string __1)
	{
		if (!__instance.IsServer())
		{
			return NonceHandshake.AllowPeerInfo(__instance, rpc, __1);
		}
		return true;
	}
}
[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
[HarmonyBefore(new string[] { "Landoria.CharacterVault" })]
internal static class ValidatePeerPatch
{
	private static bool Prefix(ZRpc rpc)
	{
		if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
		{
			return ModSentryHandshake.Admit(rpc);
		}
		return true;
	}
}
[HarmonyPatch(typeof(ZNet), "RPC_ServerSyncedPlayerData")]
internal static class RestoreServerAdmissionMarkersPatch
{
	private static void Postfix(ZRpc rpc)
	{
		SetVerifiedMarker(rpc, HandshakeState.IsAccepted(rpc));
	}

	private static void SetVerifiedMarker(ZRpc rpc, bool marked)
	{
		if (marked)
		{
			VerifiedModpackMarker.Mark(rpc);
		}
		else
		{
			VerifiedModpackMarker.Unmark(rpc);
		}
	}
}
[HarmonyPatch(typeof(ZNet), "Disconnect")]
internal static class ClearHandshakePatch
{
	private static void Prefix(ZNetPeer peer)
	{
		if (peer?.m_rpc != null)
		{
			NonceHandshake.Remove(peer.m_rpc);
			HandshakeState.Remove(peer.m_rpc);
			VerifiedModpackMarker.Unmark(peer.m_rpc);
			PendingDisconnects.Remove(peer.m_rpc);
		}
	}
}
internal sealed class HandshakeRegistry<TPeer>
{
	private readonly HashSet<TPeer> _accepted = new HashSet<TPeer>();

	private readonly Dictionary<TPeer, ValidationResult> _rejected = new Dictionary<TPeer, ValidationResult>();

	internal void Accept(TPeer peer)
	{
		_rejected.Remove(peer);
		_accepted.Add(peer);
	}

	internal void Reject(TPeer peer, ValidationResult result)
	{
		_accepted.Remove(peer);
		_rejected[peer] = result;
	}

	internal bool IsAccepted(TPeer peer)
	{
		return _accepted.Contains(peer);
	}

	internal ValidationResult RejectionFor(TPeer peer)
	{
		if (!_rejected.TryGetValue(peer, out var value))
		{
			return null;
		}
		return value;
	}

	internal void Remove(TPeer peer)
	{
		_accepted.Remove(peer);
		_rejected.Remove(peer);
	}

	internal void Clear()
	{
		_accepted.Clear();
		_rejected.Clear();
	}
}
internal static class HandshakeState
{
	private static readonly HandshakeRegistry<ZRpc> Registry = new HandshakeRegistry<ZRpc>();

	internal static void Accept(ZRpc rpc)
	{
		Registry.Accept(rpc);
	}

	internal static void Reject(ZRpc rpc, ValidationResult result)
	{
		Registry.Reject(rpc, result);
	}

	internal static bool IsAccepted(ZRpc rpc)
	{
		return Registry.IsAccepted(rpc);
	}

	internal static ValidationResult RejectionFor(ZRpc rpc)
	{
		return Registry.RejectionFor(rpc);
	}

	internal static void Remove(ZRpc rpc)
	{
		Registry.Remove(rpc);
	}

	internal static void Clear()
	{
		Registry.Clear();
	}
}
internal static class ModSentryHandshake
{
	internal static void Register(ZNet network, ZNetPeer peer)
	{
		//IL_0042: Unknown result type (might be due to invalid IL or missing references)
		//IL_004c: Expected O, but got Unknown
		NonceHandshake.Register(network, peer.m_rpc);
		if (network.IsServer())
		{
			peer.m_rpc.Register<ZPackage>("Landoria_ModSentry_Inventory", (Action<ZRpc, ZPackage>)ReceiveInventory);
			peer.m_rpc.Register("Landoria_ModSentry_RejectionAck", new Method(ReceiveRejectionAck));
		}
		else
		{
			ClientMessage.Clear();
			peer.m_rpc.Register<string>("Landoria_ModSentry_Rejection", (Action<ZRpc, string>)ClientMessage.Receive);
		}
	}

	internal static void ReceiveInventory(ZRpc rpc, ZPackage package)
	{
		if (NonceHandshake.IsFinal(rpc))
		{
			return;
		}
		try
		{
			if (NonceHandshake.Consume(rpc, package))
			{
				IReadOnlyList<PluginDescriptor> actual = PluginInventory.Deserialize(package);
				ValidationResult result = PolicyValidator.Validate(ModSentryPlugin.EnsurePolicy(), actual);
				Record(rpc, result);
			}
		}
		catch (Exception arg)
		{
			ValidationResult result2 = ValidationResult.Reject("The installed mods could not be verified.", $"Client inventory parsing failed: {arg}");
			Record(rpc, result2);
		}
	}

	internal static bool Admit(ZRpc rpc)
	{
		if (HandshakeState.IsAccepted(rpc))
		{
			return true;
		}
		ValidationResult validationResult = HandshakeState.RejectionFor(rpc);
		validationResult = validationResult ?? ValidationResult.Reject("Mod verification did not complete. Please try again.", "PeerInfo arrived before an accepted ModSentry inventory.");
		rpc.Invoke("Landoria_ModSentry_Rejection", new object[1] { validationResult.PlayerMessage });
		ModSentryPlugin.Log.LogWarning((object)validationResult.TechnicalMessage);
		PendingDisconnects.Schedule(rpc);
		return false;
	}

	internal static void RequestDisconnect(ZRpc rpc)
	{
		ModSentryPlugin.Log.LogDebug((object)"Requesting rejected pre-spawn client disconnection.");
		if (rpc != null)
		{
			rpc.Invoke("Disconnect", Array.Empty<object>());
		}
	}

	internal static void ForceDisconnect(ZRpc rpc)
	{
		ZNet instance = ZNet.instance;
		ZNetPeer val = ((instance != null) ? ((IEnumerable<ZNetPeer>)instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer candidate) => candidate.m_rpc == rpc)) : null);
		if (val != null)
		{
			ModSentryPlugin.Log.LogWarning((object)"Rejected client did not disconnect; closing the server connection.");
			ZNet.instance.Disconnect(val);
		}
	}

	internal static string Describe(ZNetPeer peer)
	{
		if (!string.IsNullOrWhiteSpace(peer?.m_playerName))
		{
			return "'" + peer.m_playerName + "'";
		}
		return "with an unavailable player name";
	}

	private static void ReceiveRejectionAck(ZRpc rpc)
	{
		PendingDisconnects.Acknowledge(rpc);
	}

	internal static void Record(ZRpc rpc, ValidationResult result)
	{
		if (result.Accepted)
		{
			HandshakeState.Accept(rpc);
			VerifiedModpackMarker.Mark(rpc);
			ModSentryPlugin.Log.LogInfo((object)result.TechnicalMessage);
		}
		else
		{
			HandshakeState.Reject(rpc, result);
			rpc.Invoke("Landoria_ModSentry_Rejection", new object[1] { result.PlayerMessage });
			PendingDisconnects.Schedule(rpc);
			ModSentryPlugin.Log.LogWarning((object)result.TechnicalMessage);
		}
	}
}
[BepInPlugin("Landoria.ModSentry", "Landoria.ModSentry", "1.0.20")]
public sealed class ModSentryPlugin : BaseUnityPlugin
{
	internal const string InventoryRpc = "Landoria_ModSentry_Inventory";

	internal const string RejectionRpc = "Landoria_ModSentry_Rejection";

	internal const string RejectionAckRpc = "Landoria_ModSentry_RejectionAck";

	internal const int ProtocolVersion = 2;

	private const string PluginGuid = "Landoria.ModSentry";

	private const string PluginName = "Landoria.ModSentry";

	private const string PluginVersion = "1.0.20";

	private Harmony _harmony;

	internal static ManualLogSource Log { get; private set; }

	internal static PluginPolicy Policy { get; private set; }

	private void Awake()
	{
		//IL_003b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0045: Expected O, but got Unknown
		Log = ((BaseUnityPlugin)this).Logger;
		((BaseUnityPlugin)this).Logger.LogInfo((object)$"AssemblyVersion: {((object)this).GetType().Assembly.GetName().Version}.");
		_harmony = new Harmony("Landoria.ModSentry");
		_harmony.PatchAll();
		PluginPolicyLoader.EnsureDirectories();
		Log.LogInfo((object)"Landoria.ModSentry 1.0.20 is loaded.");
	}

	internal static PluginPolicy EnsurePolicy()
	{
		if (Policy == null)
		{
			Policy = PluginPolicyLoader.Load();
			Log.LogInfo((object)($"Loaded {Policy.Required.Count} required and " + $"{Policy.Optional.Count} optional client mod policies."));
		}
		return Policy;
	}

	private void Update()
	{
		NonceHandshake.Tick();
		PendingDisconnects.Tick();
	}

	private void OnDestroy()
	{
		ManualLogSource log = Log;
		if (log != null)
		{
			log.LogInfo((object)"Landoria.ModSentry 1.0.20 is unloaded.");
		}
		NonceHandshake.Clear();
		HandshakeState.Clear();
		PendingDisconnects.Clear();
		ClientMessage.Clear();
		Policy = null;
		Harmony harmony = _harmony;
		if (harmony != null)
		{
			harmony.UnpatchSelf();
		}
		_harmony = null;
		Log = null;
	}
}
internal sealed class PluginDescriptor
{
	internal string Guid { get; }

	internal string Name { get; }

	internal string Version { get; }

	internal string Hash { get; }

	internal bool IsBepInPlugin { get; }

	internal PluginDescriptor(string guid, string name, string version, string hash, bool isBepInPlugin = true)
	{
		Guid = guid;
		Name = name;
		Version = version;
		Hash = hash;
		IsBepInPlugin = isBepInPlugin;
	}
}
internal static class PendingDisconnects
{
	private const float FallbackSeconds = 2f;

	private static readonly Dictionary<ZRpc, float> Deadlines = new Dictionary<ZRpc, float>();

	private static readonly HashSet<ZRpc> DisconnectRequested = new HashSet<ZRpc>();

	internal static void Schedule(ZRpc rpc)
	{
		Deadlines[rpc] = Time.unscaledTime + 2f;
	}

	internal static void Acknowledge(ZRpc rpc)
	{
		if (Deadlines.ContainsKey(rpc))
		{
			RequestDisconnect(rpc);
		}
	}

	internal static void Remove(ZRpc rpc)
	{
		Deadlines.Remove(rpc);
		DisconnectRequested.Remove(rpc);
	}

	internal static void Tick()
	{
		ZRpc[] array = (from entry in Deadlines
			where Time.unscaledTime >= entry.Value
			select entry.Key).ToArray();
		for (int num = 0; num < array.Length; num++)
		{
			AdvanceDisconnect(array[num]);
		}
	}

	internal static void Clear()
	{
		Deadlines.Clear();
		DisconnectRequested.Clear();
	}

	private static void AdvanceDisconnect(ZRpc rpc)
	{
		if (DisconnectRequested.Contains(rpc))
		{
			Remove(rpc);
			ModSentryHandshake.ForceDisconnect(rpc);
		}
		else
		{
			RequestDisconnect(rpc);
		}
	}

	private static void RequestDisconnect(ZRpc rpc)
	{
		DisconnectRequested.Add(rpc);
		Deadlines[rpc] = Time.unscaledTime + 2f;
		ModSentryHandshake.RequestDisconnect(rpc);
	}
}
internal static class NonceHandshake
{
	private sealed class Challenge
	{
		internal string Nonce;

		internal bool Started;

		internal bool Consumed;

		internal float Deadline;
	}

	private const string RequestRpc = "Landoria_ModSentry_ChallengeRequest_v2";

	private const string ChallengeRpc = "Landoria_ModSentry_Challenge_v2";

	private const float TimeoutSeconds = 30f;

	private static readonly Dictionary<ZRpc, Challenge> Challenges = new Dictionary<ZRpc, Challenge>();

	private static ZRpc serverRpc;

	private static ZNet clientNetwork;

	private static string pendingPassword;

	private static bool requested;

	private static bool sent;

	private static float clientDeadline;

	internal static void Register(ZNet network, ZRpc rpc)
	{
		if (network.IsServer())
		{
			byte[] array = new byte[32];
			using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create())
			{
				randomNumberGenerator.GetBytes(array);
			}
			Challenges[rpc] = new Challenge
			{
				Nonce = Convert.ToBase64String(array)
			};
			rpc.Register<int>("Landoria_ModSentry_ChallengeRequest_v2", (Action<ZRpc, int>)Request);
		}
		else
		{
			serverRpc = rpc;
			clientNetwork = network;
			pendingPassword = null;
			requested = (sent = false);
			rpc.Register<int, string>("Landoria_ModSentry_Challenge_v2", (Action<ZRpc, int, string>)Receive);
		}
	}

	internal static bool AllowPeerInfo(ZNet network, ZRpc rpc, string password)
	{
		if (serverRpc != rpc)
		{
			return false;
		}
		if (sent)
		{
			return true;
		}
		if (!requested)
		{
			requested = true;
			clientNetwork = network;
			pendingPassword = password;
			clientDeadline = Time.unscaledTime + 30f;
			rpc.Invoke("Landoria_ModSentry_ChallengeRequest_v2", new object[1] { 2 });
		}
		return false;
	}

	private static void Request(ZRpc rpc, int protocol)
	{
		if (Challenges.TryGetValue(rpc, out var value) && !IsFinal(rpc) && !value.Started)
		{
			value.Started = true;
			value.Deadline = Time.unscaledTime + 30f;
			if (protocol != 2)
			{
				Reject(rpc, "Incompatible or late ModSentry challenge request.");
				return;
			}
			rpc.Invoke("Landoria_ModSentry_Challenge_v2", new object[2] { 2, value.Nonce });
		}
	}

	private static void Receive(ZRpc rpc, int protocol, string nonce)
	{
		if (serverRpc != rpc || !requested || sent)
		{
			return;
		}
		try
		{
			if (protocol != 2 || nonce == null || nonce.Length != 44 || Convert.FromBase64String(nonce).Length != 32)
			{
				throw new InvalidDataException("Invalid ModSentry challenge.");
			}
			rpc.Invoke("Landoria_ModSentry_Inventory", new object[1] { PluginInventory.Serialize(nonce) });
			sent = true;
			string text = pendingPassword;
			pendingPassword = null;
			AccessTools.Method(typeof(ZNet), "SendPeerInfo", new Type[2]
			{
				typeof(ZRpc),
				typeof(string)
			}, (Type[])null).Invoke(clientNetwork, new object[2] { rpc, text });
		}
		catch (Exception ex)
		{
			ModSentryPlugin.Log.LogError((object)ex);
			FailClient("Mod verification failed. Please update ModSentry and reconnect.");
		}
	}

	internal static bool Consume(ZRpc rpc, ZPackage package)
	{
		if (!Challenges.TryGetValue(rpc, out var value) || !value.Started || value.Consumed)
		{
			Reject(rpc, "Unexpected or replayed ModSentry inventory.");
			return false;
		}
		value.Consumed = true;
		string nonce = value.Nonce;
		value.Nonce = null;
		if (Time.unscaledTime >= value.Deadline || package.ReadInt() != 2 || !string.Equals(package.ReadString(), nonce, StringComparison.Ordinal))
		{
			Reject(rpc, "Invalid or expired ModSentry inventory nonce.");
			return false;
		}
		return true;
	}

	internal static bool IsFinal(ZRpc rpc)
	{
		if (!HandshakeState.IsAccepted(rpc))
		{
			return HandshakeState.RejectionFor(rpc) != null;
		}
		return true;
	}

	private static void Reject(ZRpc rpc, string reason)
	{
		if (!IsFinal(rpc))
		{
			if (Challenges.TryGetValue(rpc, out var value))
			{
				value.Consumed = true;
				value.Nonce = null;
			}
			ModSentryHandshake.Record(rpc, ValidationResult.Reject("Mod verification failed. Please update ModSentry and reconnect.", reason));
		}
	}

	internal static void Tick()
	{
		KeyValuePair<ZRpc, Challenge>[] array = Challenges.ToArray();
		for (int i = 0; i < array.Length; i++)
		{
			KeyValuePair<ZRpc, Challenge> keyValuePair = array[i];
			if (keyValuePair.Value.Started && !keyValuePair.Value.Consumed && Time.unscaledTime >= keyValuePair.Value.Deadline)
			{
				Reject(keyValuePair.Key, "ModSentry inventory challenge timed out.");
			}
		}
		if (requested && !sent && Time.unscaledTime >= clientDeadline)
		{
			FailClient("The server did not provide a compatible ModSentry challenge. Please update and reconnect.");
		}
	}

	private static void FailClient(string message)
	{
		pendingPassword = null;
		requested = false;
		ZRpc val = serverRpc;
		serverRpc = null;
		ConnectionFailureMessages.Push("Landoria.ModSentry", message);
		if (val != null)
		{
			ModSentryHandshake.ForceDisconnect(val);
		}
	}

	internal static void Remove(ZRpc rpc)
	{
		Challenges.Remove(rpc);
		if (serverRpc == rpc)
		{
			serverRpc = null;
			clientNetwork = null;
			pendingPassword = null;
			requested = (sent = false);
		}
	}

	internal static void Clear()
	{
		pendingPassword = null;
		Challenges.Clear();
		serverRpc = null;
		clientNetwork = null;
		requested = (sent = false);
	}
}
public static class VerifiedModpackMarker
{
	private const string Key = "landoria.modsentry_verified_modpack";

	private const string Value = "1";

	public static void Mark(ZRpc rpc)
	{
		ZNetPeer val = FindPeer(rpc);
		if (val != null)
		{
			val.m_serverSyncedPlayerData["landoria.modsentry_verified_modpack"] = "1";
		}
	}

	public static bool IsMarked(string hostName)
	{
		ZNet instance = ZNet.instance;
		if (instance == null)
		{
			return false;
		}
		return instance.GetPeers().Any(delegate(ZNetPeer peer)
		{
			object obj;
			if (peer == null)
			{
				obj = null;
			}
			else
			{
				ISocket socket = peer.m_socket;
				obj = ((socket != null) ? socket.GetHostName() : null);
			}
			string value;
			return (string?)obj == hostName && peer.m_serverSyncedPlayerData.TryGetValue("landoria.modsentry_verified_modpack", out value) && value == "1";
		});
	}

	public static void Unmark(ZRpc rpc)
	{
		FindPeer(rpc)?.m_serverSyncedPlayerData.Remove("landoria.modsentry_verified_modpack");
	}

	private static ZNetPeer FindPeer(ZRpc rpc)
	{
		ZNet instance = ZNet.instance;
		if (instance == null)
		{
			return null;
		}
		return ((IEnumerable<ZNetPeer>)instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer peer) => peer.m_rpc == rpc));
	}
}
internal static class PluginInventory
{
	internal static IReadOnlyList<PluginDescriptor> Capture()
	{
		List<PluginDescriptor> list = Chainloader.PluginInfos.Values.Select((PluginInfo info) => Create(info.Metadata.GUID, info.Metadata.Name, info.Metadata.Version.ToString(), info.Location)).ToList();
		HashSet<string> pluginPaths = new HashSet<string>(Chainloader.PluginInfos.Values.Select((PluginInfo info) => Path.GetFullPath(info.Location)), StringComparer.OrdinalIgnoreCase);
		if (Directory.Exists(Paths.PluginPath))
		{
			list.AddRange((from path in Directory.GetFiles(Paths.PluginPath, "*.dll", SearchOption.AllDirectories)
				where !pluginPaths.Contains(Path.GetFullPath(path))
				select path).SelectMany(PluginPolicyLoader.ReadDescriptors));
		}
		return list.OrderBy<PluginDescriptor, string>((PluginDescriptor plugin) => plugin.Guid, StringComparer.Ordinal).ToList();
	}

	internal static ZPackage Serialize(string nonce)
	{
		//IL_0006: Unknown result type (might be due to invalid IL or missing references)
		//IL_000c: Expected O, but got Unknown
		IReadOnlyList<PluginDescriptor> readOnlyList = Capture();
		ZPackage val = new ZPackage();
		val.Write(2);
		val.Write(nonce);
		val.Write(readOnlyList.Count);
		foreach (PluginDescriptor item in readOnlyList)
		{
			Write(val, item);
		}
		return val;
	}

	internal static List<PluginDescriptor> Deserialize(ZPackage package)
	{
		int num = package.ReadInt();
		if (num < 0 || num > 1024)
		{
			throw new InvalidDataException("The client plugin count is invalid.");
		}
		List<PluginDescriptor> list = new List<PluginDescriptor>(num);
		for (int i = 0; i < num; i++)
		{
			list.Add(Read(package));
		}
		return list;
	}

	internal static string Sha256(string path)
	{
		using SHA256 sHA = SHA256.Create();
		using FileStream inputStream = File.OpenRead(path);
		return BitConverter.ToString(sHA.ComputeHash(inputStream)).Replace("-", "");
	}

	private static PluginDescriptor Create(string guid, string name, string version, string path)
	{
		return new PluginDescriptor(guid, name, version, Sha256(path));
	}

	private static void Write(ZPackage package, PluginDescriptor plugin)
	{
		package.Write(plugin.Guid);
		package.Write(plugin.Name);
		package.Write(plugin.Version);
		package.Write(plugin.Hash);
	}

	private static PluginDescriptor Read(ZPackage package)
	{
		string text = package.ReadString();
		return new PluginDescriptor(text, package.ReadString(), package.ReadString(), package.ReadString(), !text.StartsWith("Landoria.NonBepInPlugin.", StringComparison.Ordinal));
	}
}
internal sealed class PluginPolicy
{
	internal IReadOnlyList<PluginDescriptor> Required { get; }

	internal IReadOnlyList<PluginDescriptor> Optional { get; }

	internal PluginPolicy(IReadOnlyList<PluginDescriptor> required, IReadOnlyList<PluginDescriptor> optional)
	{
		Required = required;
		Optional = optional;
	}
}
internal static class PluginPolicyLoader
{
	private const string RequiredDirectoryName = "ModSentry_Required";

	private const string OptionalDirectoryName = "ModSentry_Optional";

	internal static void EnsureDirectories()
	{
		Directory.CreateDirectory(Path.Combine(Paths.ConfigPath, "ModSentry_Required"));
		Directory.CreateDirectory(Path.Combine(Paths.ConfigPath, "ModSentry_Optional"));
	}

	internal static PluginPolicy Load()
	{
		return new PluginPolicy(LoadDirectory(Path.Combine(Paths.ConfigPath, "ModSentry_Required")), LoadDirectory(Path.Combine(Paths.ConfigPath, "ModSentry_Optional")));
	}

	private static IReadOnlyList<PluginDescriptor> LoadDirectory(string directory)
	{
		if (!Directory.Exists(directory))
		{
			throw new DirectoryNotFoundException("ModSentry policy directory is missing: " + directory);
		}
		return Directory.GetFiles(directory, "*.dll", SearchOption.TopDirectoryOnly).SelectMany(ReadDescriptors).OrderBy<PluginDescriptor, string>((PluginDescriptor plugin) => plugin.Guid, StringComparer.Ordinal)
			.ToList();
	}

	internal static IReadOnlyList<PluginDescriptor> ReadDescriptors(string path)
	{
		try
		{
			AssemblyDefinition val = AssemblyDefinition.ReadAssembly(path);
			try
			{
				CustomAttribute[] array = (from item in val.MainModule.GetTypes().SelectMany((TypeDefinition type) => (IEnumerable<CustomAttribute>)type.CustomAttributes)
					where ((MemberReference)item.AttributeType).FullName == typeof(BepInPlugin).FullName
					select item).ToArray();
				if (array.Length == 0)
				{
					return new PluginDescriptor[1] { CreateFallbackDescriptor(val, path) };
				}
				string hash = PluginInventory.Sha256(path);
				return array.Select((CustomAttribute attribute) => CreateDescriptor(hash, attribute)).ToArray();
			}
			finally
			{
				((IDisposable)val)?.Dispose();
			}
		}
		catch (BadImageFormatException arg)
		{
			ModSentryPlugin.Log.LogDebug((object)$"Using a fallback descriptor for {path}: {arg}");
			return new PluginDescriptor[1] { CreateFallbackDescriptor(null, path) };
		}
	}

	private static PluginDescriptor CreateFallbackDescriptor(AssemblyDefinition assembly, string path)
	{
		object obj;
		if (assembly == null)
		{
			obj = null;
		}
		else
		{
			AssemblyNameDefinition name = assembly.Name;
			obj = ((name != null) ? ((AssemblyNameReference)name).Name : null);
		}
		if (obj == null)
		{
			obj = Path.GetFileNameWithoutExtension(path);
		}
		string text = (string)obj;
		string guid = "Landoria.NonBepInPlugin." + text;
		object obj2;
		if (assembly == null)
		{
			obj2 = null;
		}
		else
		{
			AssemblyNameDefinition name2 = assembly.Name;
			obj2 = ((name2 == null) ? null : ((AssemblyNameReference)name2).Version?.ToString());
		}
		if (obj2 == null)
		{
			obj2 = "0.0.0";
		}
		string version = (string)obj2;
		return new PluginDescriptor(guid, text, version, PluginInventory.Sha256(path), isBepInPlugin: false);
	}

	private static PluginDescriptor CreateDescriptor(string hash, CustomAttribute attribute)
	{
		//IL_0020: Unknown result type (might be due to invalid IL or missing references)
		//IL_0025: 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_0053: Unknown result type (might be due to invalid IL or missing references)
		//IL_0058: Unknown result type (might be due to invalid IL or missing references)
		if (attribute.ConstructorArguments.Count < 3)
		{
			throw new InvalidDataException("Invalid BepInPlugin metadata.");
		}
		CustomAttributeArgument val = attribute.ConstructorArguments[0];
		string guid = (string)((CustomAttributeArgument)(ref val)).Value;
		val = attribute.ConstructorArguments[1];
		string name = (string)((CustomAttributeArgument)(ref val)).Value;
		val = attribute.ConstructorArguments[2];
		string version = (string)((CustomAttributeArgument)(ref val)).Value;
		return new PluginDescriptor(guid, name, version, hash);
	}
}
internal static class PolicyValidator
{
	internal static ValidationResult Validate(PluginPolicy policy, IReadOnlyList<PluginDescriptor> actual)
	{
		Dictionary<string, PluginDescriptor> dictionary = ToDictionary(actual, "client");
		Dictionary<string, PluginDescriptor> dictionary2 = ToDictionary(policy.Required, "required");
		Dictionary<string, PluginDescriptor> dictionary3 = ToDictionary(policy.Optional, "optional");
		foreach (PluginDescriptor item in policy.Required)
		{
			if (!dictionary.TryGetValue(item.Guid, out var value))
			{
				return Missing(item);
			}
			ValidationResult validationResult = Compare(item, value, optional: false);
			if (validationResult != null)
			{
				return validationResult;
			}
		}
		foreach (PluginDescriptor item2 in actual.OrderBy<PluginDescriptor, string>((PluginDescriptor item) => item.Guid, StringComparer.Ordinal))
		{
			if (!dictionary2.ContainsKey(item2.Guid))
			{
				if (!dictionary3.TryGetValue(item2.Guid, out var value2))
				{
					return Unexpected(item2);
				}
				ValidationResult validationResult2 = Compare(value2, item2, optional: true);
				if (validationResult2 != null)
				{
					return validationResult2;
				}
			}
		}
		return ValidationResult.Accept();
	}

	private static Dictionary<string, PluginDescriptor> ToDictionary(IEnumerable<PluginDescriptor> plugins, string source)
	{
		try
		{
			return plugins.ToDictionary<PluginDescriptor, string>((PluginDescriptor item) => item.Guid, StringComparer.Ordinal);
		}
		catch (ArgumentException ex)
		{
			ModSentryPlugin.Log.LogError((object)$"Duplicate plugin GUID in {source} inventory: {ex}");
			throw new InvalidOperationException("Duplicate plugin GUID in " + source + " inventory.", ex);
		}
	}

	private static ValidationResult Compare(PluginDescriptor expected, PluginDescriptor actual, bool optional)
	{
		string text = (optional ? "optional" : "required") + " " + (expected.IsBepInPlugin ? "plugin" : "library");
		if (!string.Equals(expected.Version, actual.Version, StringComparison.Ordinal))
		{
			return ValidationResult.Reject(optional ? UpdateMessage("Optional mod mismatch", expected) : UpdateMessage("Mod update required", expected), text + " " + expected.Guid + " version mismatch: expected " + expected.Version + ", received " + actual.Version + ".");
		}
		if (!string.Equals(expected.Hash, actual.Hash, StringComparison.OrdinalIgnoreCase))
		{
			return ValidationResult.Reject(UpdateMessage("Mod mismatch", expected), text + " " + expected.Guid + " SHA-256 mismatch: expected " + expected.Hash + ", received " + actual.Hash + ".");
		}
		return null;
	}

	private static string UpdateMessage(string reason, PluginDescriptor expected)
	{
		return reason + ": " + expected.Name + " " + expected.Version + ".";
	}

	private static ValidationResult Missing(PluginDescriptor expected)
	{
		string text = (expected.IsBepInPlugin ? "mod" : "library");
		return ValidationResult.Reject(UpdateMessage("Required " + text + " missing", expected), "Required " + text + " " + expected.Guid + " " + expected.Version + " is missing.");
	}

	private static ValidationResult Unexpected(PluginDescriptor actual)
	{
		string text = (actual.IsBepInPlugin ? "mod" : "library");
		return ValidationResult.Reject("Unsupported " + text + " detected: " + actual.Name + ". Remove it before reconnecting.", "Unexpected " + text + " " + actual.Guid + " " + actual.Version + " with SHA-256 " + actual.Hash + ".");
	}
}
internal sealed class ValidationResult
{
	internal bool Accepted { get; }

	internal string PlayerMessage { get; }

	internal string TechnicalMessage { get; }

	private ValidationResult(bool accepted, string playerMessage, string technicalMessage)
	{
		Accepted = accepted;
		PlayerMessage = playerMessage;
		TechnicalMessage = technicalMessage;
	}

	internal static ValidationResult Accept()
	{
		return new ValidationResult(accepted: true, string.Empty, "Client plugin inventory accepted.");
	}

	internal static ValidationResult Reject(string playerMessage, string technicalMessage)
	{
		return new ValidationResult(accepted: false, playerMessage, technicalMessage);
	}
}
public static class ConnectionFailureMessages
{
	private const string StateKey = "Landoria.SharedLib.ConnectionFailureMessages.v1";

	private static readonly ManualLogSource Log = Logger.CreateLogSource("Landoria.ConnectionFailureMessages");

	public static void Push(string source, string message)
	{
		Push(source, message, (string)null);
	}

	public static void Push(string source, string userMessage, string systemMessage)
	{
		Push(source, userMessage, systemMessage, null);
	}

	public static void Push(string source, string userMessage, Exception exception)
	{
		Push(source, userMessage, exception?.Message, exception);
	}

	public static void Push(string source, string userMessage, string systemMessage, Exception exception)
	{
		string text = DisplayMessage(userMessage, systemMessage ?? exception?.Message);
		if (string.IsNullOrWhiteSpace(source) || text == null)
		{
			return;
		}
		bool flag = false;
		lock (AppDomain.CurrentDomain)
		{
			Stack<Tuple<string, string, string>> messages = GetMessages();
			Tuple<string, string, string> item = Tuple.Create(source, text, systemMessage);
			if (!messages.Contains(item))
			{
				messages.Push(item);
				flag = true;
			}
		}
		if (flag)
		{
			LogQueued(source, text, systemMessage, exception);
		}
	}

	public static void Clear(string source)
	{
		if (string.IsNullOrWhiteSpace(source))
		{
			return;
		}
		lock (AppDomain.CurrentDomain)
		{
			Stack<Tuple<string, string, string>> messages = GetMessages();
			Stack<Tuple<string, string, string>> stack = new Stack<Tuple<string, string, string>>();
			while (messages.Count > 0)
			{
				Tuple<string, string, string> tuple = messages.Pop();
				if (tuple.Item1 != source)
				{
					stack.Push(tuple);
				}
			}
			while (stack.Count > 0)
			{
				messages.Push(stack.Pop());
			}
		}
	}

	internal static bool TryPopAll(out string message)
	{
		lock (AppDomain.CurrentDomain)
		{
			Stack<Tuple<string, string, string>> messages = GetMessages();
			if (messages.Count > 0)
			{
				List<string> list = new List<string>();
				while (messages.Count > 0)
				{
					list.Add(messages.Pop().Item2);
				}
				message = string.Join("\n", list);
				return true;
			}
		}
		message = null;
		return false;
	}

	private static Stack<Tuple<string, string, string>> GetMessages()
	{
		if (AppDomain.CurrentDomain.GetData("Landoria.SharedLib.ConnectionFailureMessages.v1") is Stack<Tuple<string, string, string>> result)
		{
			return result;
		}
		Stack<Tuple<string, string, string>> stack = new Stack<Tuple<string, string, string>>();
		AppDomain.CurrentDomain.SetData("Landoria.SharedLib.ConnectionFailureMessages.v1", stack);
		return stack;
	}

	private static string DisplayMessage(string userMessage, string systemMessage)
	{
		if (!string.IsNullOrWhiteSpace(userMessage))
		{
			return userMessage;
		}
		if (!string.IsNullOrWhiteSpace(systemMessage))
		{
			return systemMessage;
		}
		return null;
	}

	private static void LogQueued(string source, string userMessage, string systemMessage, Exception exception)
	{
		string text = exception?.ToString() ?? Environment.StackTrace;
		if (string.IsNullOrWhiteSpace(systemMessage) || systemMessage == userMessage)
		{
			Log.LogWarning((object)("Queued connection failure message from " + source + ": " + userMessage + "\nDiagnostic stack:\n" + text));
		}
		else
		{
			Log.LogWarning((object)("Queued connection failure message from " + source + ": userMessage=" + userMessage + "; systemMessage=" + systemMessage + "\nDiagnostic stack:\n" + text));
		}
	}
}
[HarmonyPatch]
internal static class ConnectionFailureMenuPatch
{
	[HarmonyPostfix]
	[HarmonyPatch(typeof(FejdStartup), "ShowConnectError")]
	private static void ShowConnectError(TMP_Text ___m_connectionFailedError)
	{
		if (ConnectionFailureMessages.TryPopAll(out var message))
		{
			___m_connectionFailedError.text = message;
		}
	}

	[HarmonyPostfix]
	[HarmonyPatch(typeof(FejdStartup), "Start")]
	private static void Start(GameObject ___m_connectionFailedPanel, TMP_Text ___m_connectionFailedError)
	{
		ShowNext(___m_connectionFailedPanel, ___m_connectionFailedError);
	}

	[HarmonyPostfix]
	[HarmonyPatch(typeof(FejdStartup), "OnConnectionFailedOk")]
	private static void OnConnectionFailedOk(GameObject ___m_connectionFailedPanel, TMP_Text ___m_connectionFailedError)
	{
		ShowNext(___m_connectionFailedPanel, ___m_connectionFailedError);
	}

	private static void ShowNext(GameObject panel, TMP_Text text)
	{
		if (ConnectionFailureMessages.TryPopAll(out var message))
		{
			text.text = message;
			panel.SetActive(true);
		}
	}
}