Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ModSentry v1.0.20
Landoria.ModSentry.dll
Decompiled 21 hours agousing 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); } } }