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.1
Landoria.ModSentry.dll
Decompiled 4 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 Landoria.SharedLib; 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.1")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = "")] [assembly: AssemblyVersion("1.0.1.22720")] namespace Landoria.ModSentry { internal static class ClientMessage { private static string _pending; internal static void Receive(ZRpc rpc, string message) { _pending = message; rpc.Invoke("Landoria_ModSentry_RejectionAck", Array.Empty<object>()); } internal static bool TryTake(out string message) { message = _pending; _pending = null; return !string.IsNullOrWhiteSpace(message); } internal static void Clear() { _pending = null; } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] internal static class RegisterHandshakePatch { private static void Postfix(ZNet __instance, ZNetPeer peer) { ModSentryHandshake.Register(__instance, peer); } } [HarmonyPatch(typeof(ZNet), "SendPeerInfo")] internal static class SendInventoryPatch { private static void Prefix(ZRpc rpc) { if ((Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer()) { ModSentryHandshake.SendInventory(rpc); } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] 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), "Disconnect")] internal static class ClearHandshakePatch { private static void Prefix(ZNetPeer peer) { if (peer?.m_rpc != null) { HandshakeState.Remove(peer.m_rpc); PendingDisconnects.Remove(peer.m_rpc); } } } [HarmonyPatch(typeof(FejdStartup), "ShowConnectError")] internal static class ShowRejectionPatch { private static void Postfix(TMP_Text ___m_connectionFailedError) { if (ClientMessage.TryTake(out var message)) { ___m_connectionFailedError.text = message; } } } internal static class HandshakeState { private static readonly HashSet<ZRpc> Accepted = new HashSet<ZRpc>(); private static readonly Dictionary<ZRpc, ValidationResult> Rejected = new Dictionary<ZRpc, ValidationResult>(); internal static void Accept(ZRpc rpc) { Rejected.Remove(rpc); Accepted.Add(rpc); } internal static void Reject(ZRpc rpc, ValidationResult result) { Accepted.Remove(rpc); Rejected[rpc] = result; } internal static bool IsAccepted(ZRpc rpc) { return Accepted.Contains(rpc); } internal static ValidationResult RejectionFor(ZRpc rpc) { if (!Rejected.TryGetValue(rpc, out var value)) { return null; } return value; } internal static void Remove(ZRpc rpc) { Accepted.Remove(rpc); Rejected.Remove(rpc); } internal static void Clear() { Accepted.Clear(); Rejected.Clear(); } } internal static class ModSentryHandshake { internal static void Register(ZNet network, ZNetPeer peer) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Expected O, but got Unknown 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 SendInventory(ZRpc serverRpc) { serverRpc.Invoke("Landoria_ModSentry_Inventory", new object[1] { PluginInventory.Serialize() }); } internal static void ReceiveInventory(ZRpc rpc, ZPackage package) { try { 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.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(validationResult.TechnicalMessage); PendingDisconnects.Schedule(rpc); return false; } internal static void Disconnect(ZRpc rpc) { ZNetPeer val = ((IEnumerable<ZNetPeer>)ZNet.instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer candidate) => candidate.m_rpc == rpc)); if (val != null) { ZNet.instance.Kick(val.m_socket.GetHostName()); } } private static void ReceiveRejectionAck(ZRpc rpc) { if (PendingDisconnects.Acknowledge(rpc)) { Disconnect(rpc); } } private static void Record(ZRpc rpc, ValidationResult result) { if (result.Accepted) { HandshakeState.Accept(rpc); ModSentryPlugin.Log.LogInfo(result.TechnicalMessage); } else { HandshakeState.Reject(rpc, result); ModSentryPlugin.Log.LogWarning(result.TechnicalMessage); } } } [BepInPlugin("Landoria.ModSentry", "Landoria.ModSentry", "1.0.1")] public sealed class ModSentryPlugin : LandoriaPlugin { 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 = 1; private const string PluginGuid = "Landoria.ModSentry"; private const string PluginName = "Landoria.ModSentry"; private const string PluginVersion = "1.0.1"; internal static ModLog Log { get; private set; } internal static PluginPolicy Policy { get; private set; } private void Awake() { Log = InitializePlugin("Landoria.ModSentry"); Log.LogInfo("Landoria.ModSentry 1.0.1 is loaded."); } internal static PluginPolicy EnsurePolicy() { if (Policy == null) { Policy = PluginPolicy.Load(); Log.LogInfo($"Loaded {Policy.Required.Count} required and " + $"{Policy.Optional.Count} optional client mod policies."); } return Policy; } private void Update() { PendingDisconnects.Tick(); } private void OnDestroy() { Log?.LogInfo("Landoria.ModSentry 1.0.1 is unloaded."); HandshakeState.Clear(); PendingDisconnects.Clear(); ClientMessage.Clear(); Policy = null; ShutdownPlugin(); Log = null; } } internal sealed class PluginDescriptor { internal string Guid { get; } internal string Name { get; } internal string Version { get; } internal string Hash { get; } internal PluginDescriptor(string guid, string name, string version, string hash) { Guid = guid; Name = name; Version = version; Hash = hash; } } internal static class PendingDisconnects { private const float FallbackSeconds = 2f; private static readonly Dictionary<ZRpc, float> Deadlines = new Dictionary<ZRpc, float>(); internal static void Schedule(ZRpc rpc) { Deadlines[rpc] = Time.unscaledTime + 2f; } internal static bool Acknowledge(ZRpc rpc) { return Deadlines.Remove(rpc); } internal static void Remove(ZRpc rpc) { Deadlines.Remove(rpc); } internal static void Tick() { List<ZRpc> list = new List<ZRpc>(); foreach (KeyValuePair<ZRpc, float> deadline in Deadlines) { if (Time.unscaledTime >= deadline.Value) { list.Add(deadline.Key); } } foreach (ZRpc item in list) { Deadlines.Remove(item); ModSentryHandshake.Disconnect(item); } } internal static void Clear() { Deadlines.Clear(); } } internal static class PluginInventory { internal static IReadOnlyList<PluginDescriptor> Capture() { return Chainloader.PluginInfos.Values.Select((PluginInfo info) => Create(info.Metadata.GUID, info.Metadata.Name, info.Metadata.Version.ToString(), info.Location)).OrderBy<PluginDescriptor, string>((PluginDescriptor plugin) => plugin.Guid, StringComparer.Ordinal).ToList(); } internal static ZPackage Serialize() { //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(1); 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 != 1) { throw new InvalidDataException($"Unsupported ModSentry protocol {num}."); } int num2 = package.ReadInt(); if (num2 < 0 || num2 > 1024) { throw new InvalidDataException("The client plugin count is invalid."); } List<PluginDescriptor> list = new List<PluginDescriptor>(num2); for (int i = 0; i < num2; 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) { return new PluginDescriptor(package.ReadString(), package.ReadString(), package.ReadString(), package.ReadString()); } } 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 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).Select(ReadDescriptor).OrderBy<PluginDescriptor, string>((PluginDescriptor plugin) => plugin.Guid, StringComparer.Ordinal) .ToList(); } private static PluginDescriptor ReadDescriptor(string path) { AssemblyDefinition val = AssemblyDefinition.ReadAssembly(path); try { CustomAttribute attribute = ((IEnumerable<TypeDefinition>)val.MainModule.Types).SelectMany((TypeDefinition type) => (IEnumerable<CustomAttribute>)type.CustomAttributes).SingleOrDefault((Func<CustomAttribute, bool>)((CustomAttribute item) => ((MemberReference)item.AttributeType).FullName == typeof(BepInPlugin).FullName)); return CreateDescriptor(path, attribute); } finally { ((IDisposable)val)?.Dispose(); } } private static PluginDescriptor CreateDescriptor(string path, CustomAttribute attribute) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) if (attribute == null || attribute.ConstructorArguments.Count < 3) { throw new InvalidDataException("No BepInPlugin metadata was found in " + Path.GetFileName(path) + "."); } 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, PluginInventory.Sha256(path)); } } 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 innerException) { throw new InvalidOperationException("Duplicate plugin GUID in " + source + " inventory.", innerException); } } private static ValidationResult Compare(PluginDescriptor expected, PluginDescriptor actual, bool optional) { string text = (optional ? "optional" : "required"); if (!string.Equals(expected.Version, actual.Version, StringComparison.Ordinal)) { return ValidationResult.Reject(optional ? UpdateMessage("Optional mod mismatch", expected) : UpdateMessage("Mod update required", expected), text + " plugin " + 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 + " plugin " + 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) { return ValidationResult.Reject(UpdateMessage("Required mod missing", expected), "Required plugin " + expected.Guid + " " + expected.Version + " is missing."); } private static ValidationResult Unexpected(PluginDescriptor actual) { return ValidationResult.Reject("Unsupported mod detected: " + actual.Name + ". Remove it before reconnecting.", "Unexpected plugin " + 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); } } } namespace Landoria.SharedLib { public abstract class LandoriaPlugin : BaseUnityPlugin { private Harmony _harmony; private bool _patchesApplied; protected ModLog InitializePlugin(string pluginGuid) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown ModLog modLog = new ModLog(((BaseUnityPlugin)this).Logger); Version version = ((object)this).GetType().Assembly.GetName().Version; modLog.LogInfo($"AssemblyVersion: {version}."); _harmony = new Harmony(pluginGuid); PatchOwnNamespace(modLog); return modLog; } protected void PatchOwnNamespace(ModLog log) { if (_patchesApplied) { log.LogDebug("Harmony patches are already active; skipping registration."); return; } string text = ((object)this).GetType().Namespace; Type[] types = Assembly.GetExecutingAssembly().GetTypes(); foreach (Type type in types) { if (type.Namespace == text) { _harmony.CreateClassProcessor(type).Patch(); } } _patchesApplied = true; log.LogDebug("Harmony patches were applied for the plugin namespace."); } protected void ShutdownPlugin() { if (_patchesApplied) { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _patchesApplied = false; } } } public sealed class ModLog { private readonly ManualLogSource _logger; public ModLog(ManualLogSource logger) { _logger = logger; } public void LogFatal(object message) { Write((LogLevel)1, message); } public void LogError(object message) { Write((LogLevel)2, message); } public void LogWarning(object message) { Write((LogLevel)4, message); } public void LogMessage(object message) { Write((LogLevel)8, message); } public void LogInfo(object message) { Write((LogLevel)16, message); } public void LogDebug(object message) { Write((LogLevel)32, message); } public void Log(LogLevel level, object message) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) Write(level, message); } private void Write(LogLevel level, object message) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) string arg = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); _logger.Log(level, (object)$"[{arg}] {message}"); } } }