Decompiled source of Valheim ServerGuard v1.8.1

Valheim-ServerGuard.dll

Decompiled a week ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Timers;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using Newtonsoft.Json;
using UnityEngine;
using ValheimServerGuard.Shared;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]
[assembly: AssemblyCompany("yesu0725")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Valheim Server Guard - Anti-cheat and security mod for Valheim servers")]
[assembly: AssemblyFileVersion("1.8.1.0")]
[assembly: AssemblyInformationalVersion("1.8.1")]
[assembly: AssemblyProduct("Valheim-ServerGuard")]
[assembly: AssemblyTitle("Valheim-ServerGuard")]
[assembly: AssemblyVersion("1.8.1.0")]
[BepInPlugin("com.taeguk.valheim.serverguard", "Valheim ServerGuard", "1.8.1")]
public class Plugin : BaseUnityPlugin
{
	private class PingState
	{
		public bool FirstPosted;

		public List<float> Samples = new List<float>();
	}

	private class SpeedState
	{
		public Vector3 LastPos;

		public bool HasLastPos;

		public float LastSampleTime;

		public int OverThresholdCount;
	}

	private class Settings
	{
		public int ViolationThreshold { get; set; } = 3;

		public bool Enforce { get; set; } = true;

		public string KickMessage { get; set; } = "You cannot join: server security policy violation. Contact an administrator.";

		public string BanReason { get; set; } = "Auto-banned due to repeated security violations.";

		public int CharacterLimit { get; set; } = 1;

		public bool RequireCompanion { get; set; } = true;

		public int CompanionTimeoutSeconds { get; set; } = 10;

		public bool RequireHmac { get; set; } = true;

		public string SharedSecret { get; set; } = "";

		public bool AllowUnlisted { get; set; }

		public int MaxClockSkewSeconds { get; set; } = 120;

		public bool LogPeerManifest { get; set; }

		public bool EnableMetrics { get; set; } = true;

		public string discordWebhookUrl { get; set; } = "";

		public string discordWebhookUrlAdmin { get; set; } = "";

		public bool DiscordVerboseMirror { get; set; }

		public string discordChannelLink { get; set; } = "";

		public bool DailySummaryEnabled { get; set; } = true;

		public int DailySummaryHourUtc { get; set; }

		public string DailySummaryChannel { get; set; } = "admin";

		[YamlMember(Alias = "countAsViolation", ApplyNamingConventions = false)]
		public Dictionary<string, bool> CountAsViolation { get; set; } = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase)
		{
			["CompanionMissing"] = false,
			["HmacInvalid"] = false,
			["ChallengeMismatch"] = false,
			["RequiredModMissing"] = false,
			["DisallowedMod"] = false,
			["BannedMod"] = false,
			["HashMismatch"] = false,
			["CharacterNameLimitExceeded"] = true,
			["DevcommandAttempt"] = true,
			["SpeedHack"] = true,
			["ConsoleCommandBlocked"] = false,
			["IllegalItem"] = false,
			["StackOverflow"] = false,
			["AnimationCancel"] = false,
			["SkillOverflow"] = false
		};

		public bool EnableDevcommandGate { get; set; } = true;

		public bool EnableSpeedCheck { get; set; } = true;

		public double SpeedCheckMaxMetersPerSecond { get; set; } = 15.0;

		public double SpeedCheckSampleSeconds { get; set; } = 1.0;

		public int SpeedCheckConsecutiveStrikes { get; set; } = 3;

		public double SpeedCheckTeleportToleranceMeters { get; set; } = 60.0;

		public bool EnableInventoryCheck { get; set; } = true;

		public bool InventoryCheckLogOnly { get; set; } = true;

		public double InventoryCheckStackTolerance { get; set; } = 1.0;

		public bool EnableAnimationCancelGate { get; set; } = true;

		public bool EnableSkillCap { get; set; } = true;

		public double SkillCapMaxLevel { get; set; } = 100.0;

		public double SkillCapTolerance { get; set; } = 5.0;

		public bool EnableDeathLog { get; set; } = true;

		public bool EnableBuildLog { get; set; } = true;

		public int BuildLogRetentionDays { get; set; } = 30;

		public bool EnableSelfTest { get; set; } = true;

		public bool SelfTestPostOnPass { get; set; }

		public bool EnablePingLog { get; set; }

		public int PingLogSampleSeconds { get; set; } = 5;

		public bool EnableCheatItemRemoval { get; set; } = true;

		public List<string> CheatItems { get; set; } = new List<string> { "SwordCheat", "SledgeCheat" };

		public bool EnableArrivalShout { get; set; } = true;

		public bool EnableForceMapPositions { get; set; }

		public bool ForceMapPositionsExemptAdmins { get; set; }

		public bool EnableBanLayer { get; set; } = true;

		public string BanLayerKickMessage { get; set; } = "You are banned from this server.";

		public bool BanLayerMirrorToVanilla { get; set; } = true;

		public string ConsoleGuardMode { get; set; } = "restricted";

		public bool ConsoleGuardExemptModerators { get; set; } = true;

		public string ConsoleGuardBindPolicy { get; set; } = "purge";

		public List<string> ConsoleBlockedCommands { get; set; } = new List<string>();

		public List<string> ConsoleAllowedCommands { get; set; } = new List<string>();

		public bool ConsoleGuardReportAttempts { get; set; } = true;

		public bool DiscordPublicMode { get; set; } = true;

		public bool AggressiveNoModCheck { get; set; }

		public bool EnableAssemblyScanning { get; set; }

		public bool UseWhitelistMode { get; set; }

		public bool RequireAttestation { get; set; }
	}

	private class ModeratorsDoc
	{
		public List<string> moderators { get; set; } = new List<string>();

		[YamlMember(Alias = "admins", ApplyNamingConventions = false)]
		public List<string> admins { get; set; } = new List<string>();

		public IEnumerable<string> All()
		{
			return (moderators ?? new List<string>()).Concat(admins ?? new List<string>());
		}
	}

	private class OwnersDoc
	{
		public List<string> owners { get; set; } = new List<string>();
	}

	private class BansDoc
	{
		public List<BanEntry> bans { get; set; } = new List<BanEntry>();
	}

	internal class BanEntry
	{
		public string id { get; set; } = "";

		public string reason { get; set; } = "";

		public string expires { get; set; } = "";

		public string added { get; set; } = "";

		public string addedBy { get; set; } = "";

		public bool TryGetExpiry(out DateTime utc)
		{
			utc = DateTime.MinValue;
			if (string.IsNullOrWhiteSpace(expires))
			{
				return false;
			}
			return DateTime.TryParse(expires, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out utc);
		}

		public bool IsExpired(DateTime nowUtc)
		{
			if (TryGetExpiry(out var utc))
			{
				return nowUtc >= utc;
			}
			return false;
		}
	}

	private class AllowedModsDoc
	{
		[YamlMember(Alias = "required_mods", ApplyNamingConventions = false)]
		public List<string> required_mods { get; set; } = new List<string>();

		[YamlMember(Alias = "allowed_mods", ApplyNamingConventions = false)]
		public List<string> allowed_mods { get; set; } = new List<string>();

		[YamlMember(Alias = "banned_mods", ApplyNamingConventions = false)]
		public List<string> banned_mods { get; set; } = new List<string>();
	}

	private class AllowedModEntry
	{
		public string Key;

		public string Sha256;
	}

	private class PendingAttestation
	{
		public string Challenge;

		public DateTime SentAt;

		public string SteamId;

		public ZNetPeer Peer;
	}

	private class DetectionMetrics
	{
		public long total_players_checked { get; set; }

		public long total_mods_detected { get; set; }

		public long phase1_rpc_detections { get; set; }

		public long phase2_assembly_detections { get; set; }

		public long version_keyword_detections { get; set; }

		public long allowlist_bypasses { get; set; }

		public long admin_bypasses { get; set; }

		public long violations_issued { get; set; }

		public long players_banned { get; set; }

		public long ban_layer_blocks { get; set; }

		public long console_blocks { get; set; }

		public Dictionary<string, long> top_detected_mods { get; set; } = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);

		public DateTime last_updated { get; set; } = DateTime.UtcNow;
	}

	private class RegistrationsDoc
	{
		public Dictionary<string, List<string>> registrations { get; set; } = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
	}

	private class ViolationsDoc
	{
		public Dictionary<string, Dictionary<string, int>> violations { get; set; } = new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase);
	}

	private enum DiscordChannel
	{
		Public,
		Admin,
		Both
	}

	private sealed class SelfTestResult
	{
		public string Name;

		public bool Pass;

		public string Detail;
	}

	[HarmonyPatch(typeof(Inventory), "AddItem", new Type[] { typeof(ItemData) })]
	public static class Patch_Inventory_AddItem
	{
		public static bool Prefix(Inventory __instance, ItemData item)
		{
			try
			{
				if ((Object)(object)Instance == (Object)null)
				{
					return true;
				}
				Settings settings = Instance._settings;
				if (settings == null || !settings.EnableInventoryCheck)
				{
					return true;
				}
				if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer())
				{
					return true;
				}
				if (item == null || item.m_shared == null)
				{
					return true;
				}
				List<string> list = Instance.ValidateInventoryItem(item);
				if (list == null || list.Count == 0)
				{
					return true;
				}
				foreach (string item2 in list)
				{
					LogS.LogWarning((object)$"[ServerGuard] Inventory check: {item2} (logOnly={settings.InventoryCheckLogOnly})");
				}
				string text = (list[0].StartsWith("unknown item") ? "IllegalItem" : "StackOverflow");
				string text2 = list[0];
				LogS.LogWarning((object)("[ServerGuard] " + text + " - " + text2));
				if (!settings.InventoryCheckLogOnly)
				{
					return false;
				}
			}
			catch (Exception ex)
			{
				LogS.LogWarning((object)("[ServerGuard] Inventory check error: " + ex.Message));
			}
			return true;
		}
	}

	[HarmonyPatch(typeof(ZNet), "IsAllowed")]
	public static class Patch_ZNet_IsAllowed
	{
		public static void Postfix(string hostName, string playerName, ref bool __result)
		{
			try
			{
				if (__result && !((Object)(object)Instance == (Object)null) && !((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && Instance.IsBannedId(hostName, out var entry))
				{
					__result = false;
					string text = Instance.FormatPlayer(entry.id);
					string text2 = (string.IsNullOrWhiteSpace(playerName) ? "?" : playerName);
					LogS.LogWarning((object)("[ServerGuard] Refused banned SteamID at handshake: " + text + " (character '" + text2 + "') — " + entry.reason));
					Instance.PostAdminEvent(":no_entry_sign: Blocked banned **" + text + "** at connect — " + entry.reason);
					if (Instance._settings.EnableMetrics)
					{
						Instance._metrics.ban_layer_blocks++;
						Instance.SaveMetrics();
					}
				}
			}
			catch (Exception arg)
			{
				ManualLogSource logS = LogS;
				if (logS != null)
				{
					logS.LogError((object)$"[ServerGuard] IsAllowed ban gate error: {arg}");
				}
			}
		}
	}

	[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
	public static class Patch_OnNewConnection
	{
		public static void Postfix(ZNetPeer peer)
		{
			try
			{
				if (peer == null || peer.m_rpc == null || !Object.op_Implicit((Object)(object)ZNet.instance) || !ZNet.instance.IsServer())
				{
					return;
				}
				string peerSocketId = GetPeerSocketId(peer);
				if (Instance.IsBannedId(peerSocketId, out var entry))
				{
					string text = Instance.FormatPlayer(entry.id);
					LogS.LogWarning((object)("[ServerGuard] Refused banned SteamID at socket accept: " + text + " — " + entry.reason));
					Instance.PostAdminEvent(":no_entry_sign: Blocked banned **" + text + "** at socket accept — " + entry.reason);
					if (Instance._settings.EnableMetrics)
					{
						Instance._metrics.ban_layer_blocks++;
						Instance.SaveMetrics();
					}
					Instance.DisconnectAsBanned(peer);
					return;
				}
				string peerPlatformId = GetPeerPlatformId(peer);
				LogS.LogInfo((object)("[ServerGuard] Incoming connection: " + Instance.FormatPlayer(peerPlatformId)));
				peer.m_rpc.Register<string>("ServerGuard_Manifest", (Action<ZRpc, string>)delegate(ZRpc rpc, string json)
				{
					Instance.OnManifestReceived(peer, json);
				});
				peer.m_rpc.Register<string>("ServerGuard_DevcommandAttempt", (Action<ZRpc, string>)delegate(ZRpc rpc, string command)
				{
					Instance.OnDevcommandAttemptReceived(peer, command);
				});
				peer.m_rpc.Register<string>("ServerGuard_AnimationCancelAttempt", (Action<ZRpc, string>)delegate(ZRpc rpc, string source)
				{
					Instance.OnAnimationCancelReceived(peer, source);
				});
				peer.m_rpc.Register<string>("ServerGuard_SkillReport", (Action<ZRpc, string>)delegate(ZRpc rpc, string payload)
				{
					Instance.OnSkillReportReceived(peer, payload);
				});
				peer.m_rpc.Register<string>("ServerGuard_PlayerDeath", (Action<ZRpc, string>)delegate(ZRpc rpc, string payload)
				{
					Instance.OnPlayerDeathReceived(peer, payload);
				});
				peer.m_rpc.Register<string>("ServerGuard_Chat", (Action<ZRpc, string>)delegate(ZRpc rpc, string payload)
				{
					Instance.OnChatReceived(peer, payload);
				});
				peer.m_rpc.Register<string>("ServerGuard_BuildPlace", (Action<ZRpc, string>)delegate(ZRpc rpc, string payload)
				{
					Instance.OnBuildPlaceReceived(peer, payload);
				});
				peer.m_rpc.Register<string>("ServerGuard_BuildDestroy", (Action<ZRpc, string>)delegate(ZRpc rpc, string payload)
				{
					Instance.OnBuildDestroyReceived(peer, payload);
				});
				peer.m_rpc.Register<string>("ServerGuard_AdminCommand", (Action<ZRpc, string>)delegate(ZRpc rpc, string command)
				{
					Instance.OnAdminCommandReceived(peer, command);
				});
				Instance.SendArrivalShoutPolicy(peer);
				Instance.SendConsolePolicy(peer);
				if (Instance.IsAdmin(peerPlatformId))
				{
					LogS.LogInfo((object)("[ServerGuard] " + Instance.FormatPlayer(peerPlatformId) + " is " + Instance.RoleOf(peerPlatformId) + " - skipping attestation challenge."));
					if (Instance._settings.EnableMetrics)
					{
						Instance._metrics.admin_bypasses++;
						Instance.SaveMetrics();
					}
					Instance.PostPlayerEvent(Instance.IsOwner(peerPlatformId) ? ":crown:" : ":shield:", peerPlatformId, Instance.IsOwner(peerPlatformId) ? "joined as owner" : "joined as moderator");
				}
				else
				{
					if (Instance._settings.EnableMetrics)
					{
						Instance._metrics.total_players_checked++;
						Instance.SaveMetrics();
					}
					string text2 = Instance.GenerateChallenge();
					Instance.RegisterPending(peer, peerPlatformId, text2);
					peer.m_rpc.Invoke("ServerGuard_RequestManifest", new object[1] { text2 });
					((MonoBehaviour)Instance).StartCoroutine(Instance.AttestationTimeoutCoroutine(peer, peerPlatformId));
				}
			}
			catch (Exception arg)
			{
				LogS.LogError((object)$"[ServerGuard] OnNewConnection error: {arg}");
			}
		}
	}

	[HarmonyPatch(typeof(ZNet), "Disconnect")]
	public static class Patch_Disconnect
	{
		public static void Prefix(ZNetPeer peer)
		{
			try
			{
				if (peer == null || !Object.op_Implicit((Object)(object)ZNet.instance) || !ZNet.instance.IsServer() || (Object)(object)Instance == (Object)null)
				{
					return;
				}
				bool flag;
				lock (Instance._suppressLogoutFor)
				{
					flag = Instance._suppressLogoutFor.Remove(peer.m_uid);
				}
				if (!flag)
				{
					lock (Instance._pendingLock)
					{
						Instance._pending.Remove(peer.m_uid);
					}
					Instance._speedState.Remove(peer.m_uid);
					Instance._skillOverflowState.Remove(peer.m_uid);
					string peerPlatformId = GetPeerPlatformId(peer);
					if (!string.IsNullOrWhiteSpace(peerPlatformId))
					{
						string text = Instance.FormatPlayer(peerPlatformId);
						LogS.LogInfo((object)("[ServerGuard] " + text + " left the server."));
						Instance.PostPlayerEvent(":wave:", peerPlatformId, "left");
						Instance.FlushPingOnDisconnect(peer.m_uid, peerPlatformId);
					}
				}
			}
			catch (Exception ex)
			{
				LogS.LogWarning((object)("[ServerGuard] Disconnect hook error: " + ex.Message));
			}
		}
	}

	[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
	public static class Patch_RPC_PeerInfo
	{
		public static void Postfix(ZNet __instance, ZRpc rpc)
		{
			try
			{
				if (!Object.op_Implicit((Object)(object)ZNet.instance) || !ZNet.instance.IsServer())
				{
					return;
				}
				ZNetPeer val = ResolvePeerFromRpc(__instance, rpc);
				if (val == null)
				{
					return;
				}
				string peerPlatformId = GetPeerPlatformId(val);
				string charName = GetPeerPlayerName(val)?.Trim();
				if (!IsValidSteamId(peerPlatformId))
				{
					LogS.LogWarning((object)"[ServerGuard] PeerInfo without valid SteamID; deferring.");
				}
				else
				{
					if (string.IsNullOrWhiteSpace(charName) || string.Equals(charName, "Unknown", StringComparison.OrdinalIgnoreCase) || Instance.IsAdmin(peerPlatformId))
					{
						return;
					}
					if (!Instance._registrations.TryGetValue(peerPlatformId, out var value) || value == null)
					{
						value = new List<string>();
						Instance._registrations[peerPlatformId] = value;
					}
					if (value.Any((string n) => string.Equals(n, charName, StringComparison.Ordinal)))
					{
						Instance.SendCheatItemRemovalIfEnabled(val);
						return;
					}
					int num = Math.Max(1, Instance._settings.CharacterLimit);
					if (value.Count < num)
					{
						value.Add(charName);
						Instance.SaveRegistrations();
						LogS.LogInfo((object)$"[ServerGuard] Registered character #{value.Count}/{num} for {Instance.FormatPlayer(peerPlatformId)} -> '{charName}'");
						Instance.SendCheatItemRemovalIfEnabled(val);
						return;
					}
					Instance.AddViolation(peerPlatformId, "CharacterNameLimitExceeded", charName);
					if (Instance._settings.Enforce)
					{
						Instance.PostPlayerEvent(":door:", peerPlatformId, "was kicked", FriendlyReason("CharacterNameLimitExceeded"));
						Instance.TryKick(val, string.Format("{0} (Character limit {1} reached: {2})", Instance._settings.KickMessage, num, string.Join(", ", value)));
						return;
					}
					LogS.LogWarning((object)string.Format("[ServerGuard] {0} exceeded character limit ({1}). Tried '{2}'. Allowed: {3}", Instance.FormatPlayer(peerPlatformId), num, charName, string.Join(", ", value)));
				}
			}
			catch (Exception arg)
			{
				LogS.LogError((object)$"[ServerGuard] RPC_PeerInfo error: {arg}");
			}
		}
	}

	[HarmonyPatch(typeof(ZNet), "RPC_ServerSyncedPlayerData")]
	public static class Patch_ForceMapPositions
	{
		public static void Postfix(ZNet __instance, ZRpc rpc)
		{
			try
			{
				Settings settings = Instance?._settings;
				if (settings != null && settings.EnableForceMapPositions && !((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
				{
					ZNetPeer val = ResolvePeerFromRpc(__instance, rpc);
					if (val != null)
					{
						Instance.ApplyForcedMapPosition(val);
					}
				}
			}
			catch (Exception ex)
			{
				ManualLogSource logS = LogS;
				if (logS != null)
				{
					logS.LogWarning((object)("[ServerGuard] Force-map-position patch error: " + ex.Message));
				}
			}
		}
	}

	private class SkillOverflowState
	{
		public Dictionary<string, double> LastReportedLevel = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
	}

	private sealed class LastHitBox
	{
		public ZDOID Attacker;

		public string AttackerKind;

		public string AttackerName;

		public DateTime At;
	}

	[HarmonyPatch(typeof(WearNTear), "Damage")]
	public static class Patch_WearNTear_Damage_Track
	{
		public static void Prefix(WearNTear __instance, HitData hit)
		{
			//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_007d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			//IL_0082: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if ((Object)(object)Instance == (Object)null || Instance._settings == null || !Instance._settings.EnableBuildLog || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || (Object)(object)__instance == (Object)null || hit == null)
				{
					return;
				}
				ZDOID attacker = (ZDOID)(((??)GetHitAttacker(hit)) ?? ZDOID.None);
				string attackerKind = "";
				string attackerName = "";
				try
				{
					Character attacker2 = hit.GetAttacker();
					if ((Object)(object)attacker2 != (Object)null)
					{
						if (attacker2 is Player)
						{
							attackerKind = "player";
							attackerName = GetCharacterDisplayName(attacker2);
						}
						else
						{
							attackerKind = "creature";
							attackerName = GetCharacterDisplayName(attacker2);
						}
					}
				}
				catch
				{
				}
				LastHitBox value = new LastHitBox
				{
					Attacker = attacker,
					AttackerKind = attackerKind,
					AttackerName = attackerName,
					At = DateTime.UtcNow
				};
				Instance._lastHitOnPiece.Remove(__instance);
				Instance._lastHitOnPiece.Add(__instance, value);
			}
			catch
			{
			}
		}
	}

	[HarmonyPatch(typeof(WearNTear), "Destroy")]
	public static class Patch_WearNTear_Destroy_Log
	{
		public static void Prefix(WearNTear __instance)
		{
			//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0132: Unknown result type (might be due to invalid IL or missing references)
			//IL_0139: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a7: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if ((Object)(object)Instance == (Object)null || Instance._settings == null || !Instance._settings.EnableBuildLog || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || (Object)(object)__instance == (Object)null)
				{
					return;
				}
				GameObject gameObject = ((Component)__instance).gameObject;
				string text = ((gameObject != null) ? ((Object)gameObject).name : null) ?? "unknown";
				int num = text.IndexOf("(Clone)", StringComparison.Ordinal);
				if (num > 0)
				{
					text = text.Substring(0, num).Trim();
				}
				Vector3 pos;
				try
				{
					pos = ((Component)__instance).transform.position;
				}
				catch
				{
					pos = Vector3.zero;
				}
				string text2 = "";
				string text3 = "";
				if (Instance._lastHitOnPiece.TryGetValue(__instance, out var value) && value != null)
				{
					Instance._lastHitOnPiece.Remove(__instance);
					if (value.Attacker != ZDOID.None)
					{
						List<ZNetPeer> peers = ZNet.instance.GetPeers();
						if (peers != null)
						{
							foreach (ZNetPeer item in peers)
							{
								if (item != null && item.m_characterID == value.Attacker)
								{
									text2 = GetPeerPlatformId(item);
									if (Instance._registrations != null && Instance._registrations.TryGetValue(text2, out var value2) && value2 != null && value2.Count > 0)
									{
										text3 = value2[0];
									}
									break;
								}
							}
						}
					}
					if (string.IsNullOrEmpty(text2) && string.IsNullOrEmpty(text3) && !string.IsNullOrEmpty(value.AttackerName))
					{
						text3 = value.AttackerName;
					}
				}
				Instance.LogBuildEvent("destroy", text2, text3, text, pos);
			}
			catch (Exception ex)
			{
				ManualLogSource logS = LogS;
				if (logS != null)
				{
					logS.LogWarning((object)("[ServerGuard] WearNTear.Destroy hook error: " + ex.Message));
				}
			}
		}
	}

	private struct PolicyVerdict
	{
		public bool Allowed;

		public string Rule;

		public string Reason;

		public string Detail;
	}

	private sealed class DiscordLogListener : ILogListener, IDisposable
	{
		private readonly string _webhook;

		private readonly string _prefix;

		private readonly string _allowedSourceName;

		private readonly Timer _flushTimer;

		private readonly Queue<string> _buffer = new Queue<string>();

		private static readonly HttpClient _http = new HttpClient();

		private bool _isFlushing;

		private const int MaxDiscordLength = 2000;

		private const int MaxPostLength = 1800;

		public DiscordLogListener(string webhook, string prefixTag, string allowedSourceName)
		{
			_webhook = webhook?.Trim();
			_prefix = (string.IsNullOrWhiteSpace(prefixTag) ? "[ServerGuard]" : prefixTag.Trim());
			_allowedSourceName = allowedSourceName ?? string.Empty;
			_flushTimer = new Timer(2000.0);
			_flushTimer.AutoReset = true;
			_flushTimer.Elapsed += delegate
			{
				FlushIfNeeded();
			};
			_flushTimer.Start();
		}

		public void LogEvent(object sender, LogEventArgs eventArgs)
		{
			//IL_0041: 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)
			try
			{
				if (string.IsNullOrWhiteSpace(_webhook))
				{
					return;
				}
				ILogSource source = eventArgs.Source;
				if (!string.Equals(((source != null) ? source.SourceName : null) ?? string.Empty, _allowedSourceName, StringComparison.Ordinal))
				{
					return;
				}
				string text = ((object)eventArgs.Level/*cast due to .constrained prefix*/).ToString().ToUpperInvariant();
				string text2 = eventArgs.Data?.ToString() ?? "";
				string item = (_prefix + " [" + text + "] " + text2).Trim();
				lock (_buffer)
				{
					_buffer.Enqueue(item);
					if (_buffer.Count > 1000)
					{
						_buffer.Dequeue();
					}
				}
			}
			catch
			{
			}
		}

		private async void FlushIfNeeded()
		{
			if (string.IsNullOrWhiteSpace(_webhook) || _isFlushing)
			{
				return;
			}
			List<string> list = null;
			lock (_buffer)
			{
				if (_buffer.Count == 0)
				{
					return;
				}
				list = new List<string>(_buffer);
				_buffer.Clear();
			}
			_isFlushing = true;
			try
			{
				StringBuilder chunk = new StringBuilder();
				foreach (string line in list)
				{
					int num = line.Length + 1;
					if (chunk.Length + num > 1800)
					{
						await PostAsync(chunk.ToString());
						chunk.Clear();
					}
					chunk.AppendLine((line.Length > 2000) ? line.Substring(0, 2000) : line);
				}
				if (chunk.Length > 0)
				{
					await PostAsync(chunk.ToString());
				}
			}
			catch
			{
			}
			finally
			{
				_isFlushing = false;
			}
		}

		private async Task PostAsync(string content)
		{
			if (!string.IsNullOrWhiteSpace(content))
			{
				string text = JsonConvert.SerializeObject((object)new { content });
				StringContent req = new StringContent(text, Encoding.UTF8, "application/json");
				try
				{
					await _http.PostAsync(_webhook, (HttpContent)(object)req);
				}
				finally
				{
					((IDisposable)req)?.Dispose();
				}
			}
		}

		public void Dispose()
		{
			try
			{
				_flushTimer?.Stop();
				_flushTimer?.Dispose();
			}
			catch
			{
			}
		}
	}

	[HarmonyPatch]
	public static class Patch_SetRandomEvent
	{
		private static MethodBase TargetMethod()
		{
			try
			{
				MethodInfo? method = typeof(RandEventSystem).GetMethod("SetRandomEvent", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (method == null)
				{
					ManualLogSource logS = LogS;
					if (logS != null)
					{
						logS.LogWarning((object)"[ServerGuard] RandEventSystem.SetRandomEvent not found — raid start logging unavailable.");
					}
				}
				return method;
			}
			catch (Exception ex)
			{
				ManualLogSource logS2 = LogS;
				if (logS2 != null)
				{
					logS2.LogWarning((object)("[ServerGuard] Failed to locate RandEventSystem.SetRandomEvent: " + ex.Message));
				}
				return null;
			}
		}

		public static void Postfix(RandomEvent ev, Vector3 pos)
		{
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && ev != null && !string.IsNullOrEmpty(ev.m_name))
				{
					Instance.OnRaidStarted(ev.m_name, pos);
				}
			}
			catch (Exception ex)
			{
				ManualLogSource logS = LogS;
				if (logS != null)
				{
					logS.LogError((object)("[ServerGuard] SetRandomEvent patch error: " + ex.Message));
				}
			}
		}
	}

	[HarmonyPatch(typeof(RandEventSystem), "ResetRandomEvent")]
	public static class Patch_ResetRandomEvent
	{
		public static void Prefix()
		{
			try
			{
				if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
				{
					Instance.OnRaidEnded();
				}
			}
			catch (Exception ex)
			{
				ManualLogSource logS = LogS;
				if (logS != null)
				{
					logS.LogError((object)("[ServerGuard] ResetRandomEvent patch error: " + ex.Message));
				}
			}
		}
	}

	internal static Plugin Instance;

	internal static ManualLogSource LogS;

	private Harmony _harmony;

	private DiscordLogListener _discordListener;

	private static readonly string RootDir = Path.Combine(Paths.ConfigPath, "ServerGuard");

	private static readonly string ConfDir = Path.Combine(RootDir, "conf");

	private static readonly string BuildLogDir = Path.Combine(RootDir, "build_log");

	private static readonly string ReadmeMD = Path.Combine(RootDir, "README.md");

	private static readonly string SettingsYaml = Path.Combine(ConfDir, "settings.yaml");

	private static readonly string ModeratorsYaml = Path.Combine(ConfDir, "moderators.yaml");

	private static readonly string OwnersYaml = Path.Combine(ConfDir, "owners.yaml");

	private static readonly string LegacyAdminsYaml = Path.Combine(ConfDir, "admins.yaml");

	private static readonly string BansYaml = Path.Combine(ConfDir, "bans.yaml");

	private static readonly string AllowedModsYaml = Path.Combine(ConfDir, "allowed_mods.yaml");

	private static readonly string RegistrationsYaml = Path.Combine(ConfDir, "registrations.yaml");

	private static readonly string ViolationsYaml = Path.Combine(ConfDir, "violations.yaml");

	private static readonly string MetricsYaml = Path.Combine(ConfDir, "metrics.yaml");

	private static readonly string LegacyIgnoreModsYaml = Path.Combine(ConfDir, "ignore_mods.yaml");

	private static readonly string LegacyModPatternsYaml = Path.Combine(ConfDir, "mod_patterns.yaml");

	private static IDeserializer _yamlIn;

	private static ISerializer _yamlOut;

	private static ISerializer _yamlOutFull;

	private Settings _settings;

	private HashSet<string> _admins = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

	private HashSet<string> _owners = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

	private DetectionMetrics _metrics;

	private Dictionary<string, BanEntry> _bans = new Dictionary<string, BanEntry>(StringComparer.OrdinalIgnoreCase);

	private readonly object _bansLock = new object();

	private List<AllowedModEntry> _requiredMods = new List<AllowedModEntry>();

	private List<AllowedModEntry> _allowedMods = new List<AllowedModEntry>();

	private List<AllowedModEntry> _bannedMods = new List<AllowedModEntry>();

	private string _modsetFingerprintStrict = "";

	private string _modsetFingerprintLoose = "";

	private Dictionary<long, PendingAttestation> _pending = new Dictionary<long, PendingAttestation>();

	private readonly object _pendingLock = new object();

	private readonly HashSet<long> _suppressLogoutFor = new HashSet<long>();

	private readonly Dictionary<long, PingState> _pingState = new Dictionary<long, PingState>();

	private static FieldInfo _rpcPingField;

	private readonly Dictionary<long, SpeedState> _speedState = new Dictionary<long, SpeedState>();

	private Dictionary<string, List<string>> _registrations = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);

	private Dictionary<string, Dictionary<string, int>> _violations = new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase);

	private const string RULE_COMPANION_MISSING = "CompanionMissing";

	private const string RULE_HMAC_INVALID = "HmacInvalid";

	private const string RULE_CHALLENGE_MISMATCH = "ChallengeMismatch";

	private const string RULE_REQUIRED_MOD_MISSING = "RequiredModMissing";

	private const string RULE_DISALLOWED_MOD = "DisallowedMod";

	private const string RULE_BANNED_MOD = "BannedMod";

	private const string RULE_HASH_MISMATCH = "HashMismatch";

	private const string RULE_CHAR_NAME_LIMIT = "CharacterNameLimitExceeded";

	private const string RULE_DEVCOMMAND_ATTEMPT = "DevcommandAttempt";

	private const string RULE_CONSOLE_COMMAND = "ConsoleCommandBlocked";

	private const string RULE_SPEED_HACK = "SpeedHack";

	private const string RULE_ILLEGAL_ITEM = "IllegalItem";

	private const string RULE_STACK_OVERFLOW = "StackOverflow";

	private const string RULE_ANIMATION_CANCEL = "AnimationCancel";

	private const string RULE_SKILL_OVERFLOW = "SkillOverflow";

	private static readonly string[] ALL_RULES = new string[15]
	{
		"CompanionMissing", "HmacInvalid", "ChallengeMismatch", "RequiredModMissing", "DisallowedMod", "BannedMod", "HashMismatch", "CharacterNameLimitExceeded", "DevcommandAttempt", "ConsoleCommandBlocked",
		"SpeedHack", "IllegalItem", "StackOverflow", "AnimationCancel", "SkillOverflow"
	};

	private FileSystemWatcher _watchSettings;

	private FileSystemWatcher _watchAdmins;

	private FileSystemWatcher _watchOwners;

	private FileSystemWatcher _watchAllowed;

	private FileSystemWatcher _watchBans;

	private readonly Dictionary<string, DateTime> _lastSeenWrite = new Dictionary<string, DateTime>();

	private string _currentRaidName;

	private Vector3 _currentRaidPos = Vector3.zero;

	private bool _raidPaused;

	private Coroutine _raidMonitorCoroutine;

	private static readonly Dictionary<string, string> RaidDisplayNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
	{
		["army_eikthyr"] = "Eikthyr Rallies His Herd",
		["army_theelder"] = "The Forest Is Moving",
		["army_bonemass"] = "A Foul Smell From the Swamp",
		["army_moder"] = "A Cold Wind Blows From the Mountains",
		["army_goblin"] = "The Horde Is Attacking",
		["skeletons"] = "Skeleton Surprise",
		["blobs"] = "The Ooze Bomb",
		["foresttrolls"] = "The Ground Is Shaking",
		["wolves"] = "You Are Being Hunted",
		["bats"] = "Bat Attack",
		["surtlings"] = "It's Raining Fire",
		["army_gjall"] = "Mistlands Quiver",
		["army_gsecret"] = "Seeker Swarm",
		["army_dverger"] = "Dverger Invasion",
		["army_charred"] = "Charred Assault",
		["army_fallen"] = "The Fallen March",
		["army_asksvin"] = "Asksvin Attack"
	};

	private string _attachedAdminWebhookUrl = "";

	private bool _bootCompleted;

	private bool _dailySummaryStarted;

	private readonly object _summaryLock = new object();

	private DateTime _summarySince = DateTime.UtcNow;

	private int _summaryJoins;

	private int _summaryKicks;

	private int _summaryBans;

	private readonly Dictionary<string, int> _summaryKickReasons = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

	private int _summaryLeaves;

	private readonly Dictionary<long, SkillOverflowState> _skillOverflowState = new Dictionary<long, SkillOverflowState>();

	private readonly ConditionalWeakTable<WearNTear, LastHitBox> _lastHitOnPiece = new ConditionalWeakTable<WearNTear, LastHitBox>();

	private static FieldInfo _hitAttackerField;

	private const int AdminReplyMaxLines = 25;

	private const int BuildQueryDefaultDays = 7;

	private const int BuildQueryMaxResults = 20;

	private static readonly HashSet<string> _animationCancelIgnoredSources = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "sheathe" };

	private static readonly string[] VALID_CONSOLE_MODES = new string[4] { "open", "restricted", "whitelist", "disabled" };

	private static readonly string[] VALID_BIND_POLICIES = new string[4] { "allow", "block", "purge", "wipe" };

	private static string GetRaidDisplayName(string internalName)
	{
		if (string.IsNullOrEmpty(internalName))
		{
			return internalName;
		}
		if (!RaidDisplayNames.TryGetValue(internalName, out var value))
		{
			return internalName;
		}
		return value;
	}

	private void Awake()
	{
		//IL_0011: Unknown result type (might be due to invalid IL or missing references)
		//IL_0020: Expected O, but got Unknown
		//IL_002f: Unknown result type (might be due to invalid IL or missing references)
		//IL_003e: Expected O, but got Unknown
		//IL_004e: Unknown result type (might be due to invalid IL or missing references)
		//IL_005d: Expected O, but got Unknown
		//IL_00af: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b9: Expected O, but got Unknown
		Instance = this;
		LogS = ((BaseUnityPlugin)this).Logger;
		_yamlIn = ((BuilderSkeleton<DeserializerBuilder>)new DeserializerBuilder()).WithNamingConvention(CamelCaseNamingConvention.Instance).IgnoreUnmatchedProperties().Build();
		_yamlOut = ((BuilderSkeleton<SerializerBuilder>)new SerializerBuilder()).WithNamingConvention(CamelCaseNamingConvention.Instance).ConfigureDefaultValuesHandling((DefaultValuesHandling)2).Build();
		_yamlOutFull = ((BuilderSkeleton<SerializerBuilder>)new SerializerBuilder()).WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
		EnsureFoldersAndFiles();
		LoadSettings();
		TopUpSettingsFile();
		LoadOwners();
		LoadAdmins();
		LoadBans();
		LoadAllowedMods();
		LoadRegistrations();
		LoadViolations();
		LoadMetrics();
		StartWatchers();
		_harmony = new Harmony("com.taeguk.valheim.serverguard");
		_harmony.PatchAll();
		LogS.LogInfo((object)("[ServerGuard] Loaded (v1.8.1). Enforcement: " + (_settings.Enforce ? "ON" : "LOG-ONLY") + ". RequireCompanion: " + (_settings.RequireCompanion ? "ON" : "OFF") + ". RequireHmac: " + (_settings.RequireHmac ? "ON" : "OFF") + ". AllowUnlisted: " + (_settings.AllowUnlisted ? "ON" : "OFF") + ". " + $"Required: {_requiredMods.Count}, Allowed: {_allowedMods.Count}, Banned: {_bannedMods.Count}. " + $"Owners: {_owners.Count}, Moderators: {_admins.Count}. " + "Metrics: " + (_settings.EnableMetrics ? "ON" : "OFF")));
		if (_owners.Count == 0)
		{
			LogS.LogWarning((object)"[ServerGuard] No owner configured. Add your SteamID64 to conf/owners.yaml to be exempt from every rule.");
		}
		if (_settings.RequireHmac && !string.IsNullOrEmpty(_settings.SharedSecret))
		{
			LogS.LogInfo((object)("[ServerGuard] sharedSecret in use (copy to every client.yaml): " + _settings.SharedSecret));
		}
		ReconfigureDiscordAndSummary();
		((MonoBehaviour)this).StartCoroutine(SpeedCheckLoop());
		((MonoBehaviour)this).StartCoroutine(BuildLogCleanupLoop());
		((MonoBehaviour)this).StartCoroutine(PingLogLoop());
		if (_settings.EnableSelfTest)
		{
			try
			{
				List<SelfTestResult> list = RunSelfTest();
				LogS.LogInfo((object)FormatSelfTestReport(list));
				if (list.Any((SelfTestResult r) => !r.Pass) || _settings.SelfTestPostOnPass)
				{
					PostAdminEvent(FormatSelfTestForDiscord(list));
				}
			}
			catch (Exception arg)
			{
				LogS.LogError((object)$"[ServerGuard] Self-test failed to run: {arg}");
			}
		}
		PostAdminEvent(":rocket: **ServerGuard online** v1.8.1  enforce=" + (_settings.Enforce ? "ON" : "off") + "  requireHmac=" + (_settings.RequireHmac ? "ON" : "off") + "  " + $"req/allow/ban={_requiredMods.Count}/{_allowedMods.Count}/{_bannedMods.Count}  " + "modset=`" + ModsetFingerprint.Short(_modsetFingerprintLoose) + "`");
		SendDiscordNow(":hourglass_flowing_sand: **Server is starting...**");
		((MonoBehaviour)this).StartCoroutine(ServerReadyWatcher());
		_bootCompleted = true;
		if (_settings.EnableSpeedCheck)
		{
			LogS.LogInfo((object)($"[ServerGuard] Speed check enabled  threshold={_settings.SpeedCheckMaxMetersPerSecond:F1}m/s  " + $"sample={_settings.SpeedCheckSampleSeconds:F1}s  " + $"strikes={_settings.SpeedCheckConsecutiveStrikes}  " + $"teleport-tol={_settings.SpeedCheckTeleportToleranceMeters:F1}m"));
		}
		if (_settings.EnableForceMapPositions)
		{
			LogS.LogInfo((object)("[ServerGuard] Forced map positions enabled  exemptAdmins=" + (_settings.ForceMapPositionsExemptAdmins ? "yes" : "no")));
		}
	}

	private static bool IsServerReadyForPlayers()
	{
		try
		{
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null || !instance.IsServer())
			{
				return false;
			}
			ZoneSystem instance2 = ZoneSystem.instance;
			if ((Object)(object)instance2 == (Object)null)
			{
				return false;
			}
			return instance2.LocationsGenerated;
		}
		catch
		{
			return false;
		}
	}

	private IEnumerator ServerReadyWatcher()
	{
		float deadline = Time.realtimeSinceStartup + 900f;
		while (Time.realtimeSinceStartup < deadline)
		{
			if (IsServerReadyForPlayers())
			{
				LogS.LogInfo((object)"[ServerGuard] Server is ready for players.");
				SendDiscordNow(":white_check_mark: **The server has started, you may now login.**");
				yield break;
			}
			yield return (object)new WaitForSeconds(1f);
		}
		LogS.LogWarning((object)"[ServerGuard] Server-ready watcher timed out after 15 minutes; skipping the public 'server started' post.");
		PostAdminEvent(":warning: Server-ready watcher timed out - no public \"server started\" message was sent. Check whether world generation finished.");
	}

	private void PostShutdownNoticeBlocking()
	{
		//IL_0039: Unknown result type (might be due to invalid IL or missing references)
		//IL_003f: Expected O, but got Unknown
		//IL_003f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0044: Unknown result type (might be due to invalid IL or missing references)
		//IL_0059: Expected O, but got Unknown
		try
		{
			string text = _settings?.discordWebhookUrl;
			if (string.IsNullOrWhiteSpace(text))
			{
				return;
			}
			StringContent val = new StringContent(JsonConvert.SerializeObject((object)new
			{
				content = ":octagonal_sign: **Server is shutting down.**"
			}), Encoding.UTF8, "application/json");
			try
			{
				HttpClient val2 = new HttpClient
				{
					Timeout = TimeSpan.FromSeconds(5.0)
				};
				try
				{
					val2.PostAsync(text, (HttpContent)(object)val).GetAwaiter().GetResult();
				}
				finally
				{
					((IDisposable)val2)?.Dispose();
				}
			}
			finally
			{
				((IDisposable)val)?.Dispose();
			}
		}
		catch (Exception ex)
		{
			ManualLogSource logS = LogS;
			if (logS != null)
			{
				logS.LogWarning((object)("[ServerGuard] Shutdown notice failed: " + ex.Message));
			}
		}
	}

	private void OnDestroy()
	{
		PostShutdownNoticeBlocking();
		try
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
		catch (Exception ex)
		{
			ManualLogSource logS = LogS;
			if (logS != null)
			{
				logS.LogWarning((object)("[ServerGuard] UnpatchSelf failed: " + ex.Message));
			}
		}
		try
		{
			if (_discordListener != null)
			{
				try
				{
					Logger.Listeners.Remove((ILogListener)(object)_discordListener);
				}
				catch (Exception ex2)
				{
					ManualLogSource logS2 = LogS;
					if (logS2 != null)
					{
						logS2.LogWarning((object)("[ServerGuard] Removing Discord listener failed: " + ex2.Message));
					}
				}
				try
				{
					_discordListener.Dispose();
				}
				catch (Exception ex3)
				{
					ManualLogSource logS3 = LogS;
					if (logS3 != null)
					{
						logS3.LogWarning((object)("[ServerGuard] Disposing Discord listener failed: " + ex3.Message));
					}
				}
				_discordListener = null;
			}
		}
		catch (Exception ex4)
		{
			ManualLogSource logS4 = LogS;
			if (logS4 != null)
			{
				logS4.LogWarning((object)("[ServerGuard] Discord listener cleanup failed: " + ex4.Message));
			}
		}
		try
		{
			StopWatchers();
		}
		catch (Exception ex5)
		{
			ManualLogSource logS5 = LogS;
			if (logS5 != null)
			{
				logS5.LogWarning((object)("[ServerGuard] StopWatchers failed: " + ex5.Message));
			}
		}
		try
		{
			SaveAll();
		}
		catch (Exception ex6)
		{
			ManualLogSource logS6 = LogS;
			if (logS6 != null)
			{
				logS6.LogWarning((object)("[ServerGuard] SaveAll failed: " + ex6.Message));
			}
		}
	}

	private async Task PostToWebhook(string url, string text)
	{
		if (string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(text))
		{
			return;
		}
		try
		{
			HttpClient http = new HttpClient();
			try
			{
				string text2 = JsonConvert.SerializeObject((object)new
				{
					content = text
				});
				StringContent req = new StringContent(text2, Encoding.UTF8, "application/json");
				try
				{
					await http.PostAsync(url, (HttpContent)(object)req);
				}
				finally
				{
					((IDisposable)req)?.Dispose();
				}
			}
			finally
			{
				((IDisposable)http)?.Dispose();
			}
		}
		catch (Exception ex)
		{
			LogS.LogWarning((object)("[ServerGuard] PostToWebhook failed: " + ex.Message));
		}
	}

	private async Task SendDiscordNow(string text, DiscordChannel target = DiscordChannel.Public)
	{
		if (_settings == null)
		{
			return;
		}
		string pub = _settings.discordWebhookUrl;
		string adm = _settings.discordWebhookUrlAdmin;
		switch (target)
		{
		case DiscordChannel.Public:
			await PostToWebhook(pub, text);
			break;
		case DiscordChannel.Admin:
			await PostToWebhook(string.IsNullOrWhiteSpace(adm) ? pub : adm, text);
			break;
		case DiscordChannel.Both:
			await PostToWebhook(pub, text);
			if (!string.IsNullOrWhiteSpace(adm) && !string.Equals(adm, pub, StringComparison.Ordinal))
			{
				await PostToWebhook(adm, text);
			}
			break;
		}
	}

	private void EnsureFoldersAndFiles()
	{
		Directory.CreateDirectory(RootDir);
		Directory.CreateDirectory(ConfDir);
		if (!File.Exists(SettingsYaml))
		{
			Settings settings = new Settings
			{
				SharedSecret = GenerateSharedSecret()
			};
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("# ServerGuard settings (v1.8.1)");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Client-attestation handshake:");
			stringBuilder.AppendLine("#   requireCompanion       - if true, peers without the ServerGuard.Client plugin are kicked.");
			stringBuilder.AppendLine("#   companionTimeoutSeconds - how long to wait for the manifest before declaring 'no companion'.");
			stringBuilder.AppendLine("#   requireHmac            - if true, manifests must carry a valid HMAC signature.");
			stringBuilder.AppendLine("#   sharedSecret           - secret string. Must match every client's client.yaml `sharedSecret`.");
			stringBuilder.AppendLine("#                            Generate something long and random (e.g. `openssl rand -hex 32`).");
			stringBuilder.AppendLine("#   allowUnlisted          - if true, mods absent from allowed_mods.yaml are tolerated.");
			stringBuilder.AppendLine("#                            Default false = strict allowlist.");
			stringBuilder.AppendLine("#   maxClockSkewSeconds    - reject manifests whose timestamp is more than this off from server time.");
			stringBuilder.AppendLine("#   logPeerManifest        - if true, log every connecting peer's full manifest (verbose).");
			stringBuilder.AppendLine("#                            Useful for harvesting plugin GUIDs to populate allowed_mods.yaml.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Identity / character limits:");
			stringBuilder.AppendLine("#   characterLimit         - max distinct character names a SteamID may use on this server.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Devcommands gate (anti-cheat):");
			stringBuilder.AppendLine("#   enableDevcommandGate   - if true, devcommand attempts reported by the companion");
			stringBuilder.AppendLine("#                            plugin are logged + posted + counted. The companion");
			stringBuilder.AppendLine("#                            ALWAYS blocks `devcommands` and forces");
			stringBuilder.AppendLine("#                            Console.IsCheatsEnabled=false on multiplayer clients;");
			stringBuilder.AppendLine("#                            this toggle only affects server-side accounting.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Movement-speed sanity check (anti-cheat):");
			stringBuilder.AppendLine("#   enableSpeedCheck                  - master toggle.");
			stringBuilder.AppendLine("#   speedCheckMaxMetersPerSecond      - horizontal speed cap. Vanilla sprint ~5 m/s,");
			stringBuilder.AppendLine("#                                       longship sail ~9-10 m/s. 15 m/s is a generous");
			stringBuilder.AppendLine("#                                       default; raise for modded mounts/skills.");
			stringBuilder.AppendLine("#   speedCheckSampleSeconds           - poll interval. Lower = faster detection,");
			stringBuilder.AppendLine("#                                       more sensitive to lag spikes. 1.0 is balanced.");
			stringBuilder.AppendLine("#   speedCheckConsecutiveStrikes      - over-threshold samples needed to fire SpeedHack.");
			stringBuilder.AppendLine("#   speedCheckTeleportToleranceMeters - single-sample displacements larger than this");
			stringBuilder.AppendLine("#                                       are treated as legitimate teleports (portals,");
			stringBuilder.AppendLine("#                                       stones) and reset the strike counter rather");
			stringBuilder.AppendLine("#                                       than incrementing it.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Inventory item validation (anti-cheat):");
			stringBuilder.AppendLine("#   enableInventoryCheck         - master toggle for Inventory.AddItem validation.");
			stringBuilder.AppendLine("#   inventoryCheckLogOnly        - if true (default), invalid items are logged but");
			stringBuilder.AppendLine("#                                  still added. Flip to false to actively block them.");
			stringBuilder.AppendLine("#                                  Start in log-only mode to audit false positives,");
			stringBuilder.AppendLine("#                                  then tighten.");
			stringBuilder.AppendLine("#   inventoryCheckStackTolerance - multiplier on each item's m_maxStackSize. 1.0 is");
			stringBuilder.AppendLine("#                                  strict; 2.0 allows up to 2x the vanilla cap for");
			stringBuilder.AppendLine("#                                  modpacks that legitimately raise limits.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Animation-cancel gate (anti-cheat):");
			stringBuilder.AppendLine("#   enableAnimationCancelGate - if true, attempts to cancel attack-recovery");
			stringBuilder.AppendLine("#                               animations (emote) reported by the companion");
			stringBuilder.AppendLine("#                               are logged + posted + counted.");
			stringBuilder.AppendLine("#                               The companion ALWAYS blocks the cancel client-side;");
			stringBuilder.AppendLine("#                               this toggle only controls server-side accounting.");
			stringBuilder.AppendLine("#                               Sheathing is NOT part of this rule - weapon swaps,");
			stringBuilder.AppendLine("#                               looting and building all holster the weapon.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Skill-level cap (anti-cheat):");
			stringBuilder.AppendLine("#   enableSkillCap     - master toggle. Companion plugin sends a snapshot of");
			stringBuilder.AppendLine("#                        each player's m_skills every ~60s; server flags any");
			stringBuilder.AppendLine("#                        skill above the cap.");
			stringBuilder.AppendLine("#   skillCapMaxLevel   - max allowed level. Vanilla is 100.");
			stringBuilder.AppendLine("#   skillCapTolerance  - added to skillCapMaxLevel to form the actual flag");
			stringBuilder.AppendLine("#                        threshold. Use to absorb float rounding / minor over-shoot.");
			stringBuilder.AppendLine("#                        Raise both for modpacks that legitimately allow higher.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Death log (public Discord):");
			stringBuilder.AppendLine("#   enableDeathLog     - if true, posts a public-channel message every time a");
			stringBuilder.AppendLine("#                        player dies. Includes position and killer (player name");
			stringBuilder.AppendLine("#                        + SteamID for PvP, creature name for mobs, cause for");
			stringBuilder.AppendLine("#                        environmental). Pure forensic log - no violation rule.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Build / destroy heatmap:");
			stringBuilder.AppendLine("#   enableBuildLog        - if true, every piece placement and destruction is");
			stringBuilder.AppendLine("#                           appended to a daily CSV file at");
			stringBuilder.AppendLine("#                           BepInEx/config/ServerGuard/build_log/YYYY-MM-DD.csv.");
			stringBuilder.AppendLine("#                           Useful for investigating grief reports. No Discord");
			stringBuilder.AppendLine("#                           output, no violation rule - pure forensic log.");
			stringBuilder.AppendLine("#   buildLogRetentionDays - delete CSV files older than this. Default 30.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Self-test (boot-time smoke checks):");
			stringBuilder.AppendLine("#   enableSelfTest      - run a suite of smoke tests (HMAC, fingerprint,");
			stringBuilder.AppendLine("#                         build-log dir, webhook syntax, ...) at startup.");
			stringBuilder.AppendLine("#                         Result is logged and posted to admin Discord on FAIL.");
			stringBuilder.AppendLine("#                         Re-run on demand via the `sg selftest` console cmd.");
			stringBuilder.AppendLine("#   selfTestPostOnPass  - if true, also post a green-checkmark line to admin");
			stringBuilder.AppendLine("#                         even when all tests pass. Default false (only FAILs).");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Ping / latency log:");
			stringBuilder.AppendLine("#   enablePingLog        - if true, sample each peer's RTT and post the first");
			stringBuilder.AppendLine("#                          measurement after join + session avg on disconnect");
			stringBuilder.AppendLine("#                          to the admin channel. Useful for proxy / VPN spotting.");
			stringBuilder.AppendLine("#                          Default false.");
			stringBuilder.AppendLine("#   pingLogSampleSeconds - sampling interval. Default 5.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Cheat item removal:");
			stringBuilder.AppendLine("#   enableCheatItemRemoval - if true, the companion strips the items listed in");
			stringBuilder.AppendLine("#                            cheatItems from any non-admin player's inventory on login.");
			stringBuilder.AppendLine("#   cheatItems             - prefab names to remove (default: SwordCheat, SledgeCheat).");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Arrival shout:");
			stringBuilder.AppendLine("#   enableArrivalShout - vanilla makes every player shout \"I have arrived!\" the");
			stringBuilder.AppendLine("#                        first time they spawn in. Set to false and the companion");
			stringBuilder.AppendLine("#                        swallows it - useful when the server already announces");
			stringBuilder.AppendLine("#                        logins and the shout is just noise. Players can still");
			stringBuilder.AppendLine("#                        shout manually. Default true (vanilla behaviour).");
			stringBuilder.AppendLine("#                        Hot-reloads: online players pick up the change at once.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Forced map positions:");
			stringBuilder.AppendLine("#   enableForceMapPositions       - if true, every player is permanently visible on");
			stringBuilder.AppendLine("#                                   everyone's map, regardless of their own");
			stringBuilder.AppendLine("#                                   'public position' minimap toggle. Enforced");
			stringBuilder.AppendLine("#                                   server-side, so a modified client can't opt out.");
			stringBuilder.AppendLine("#                                   Default false (vanilla behaviour: each player");
			stringBuilder.AppendLine("#                                   chooses). Takes effect within ~2s of a reload -");
			stringBuilder.AppendLine("#                                   no restart needed.");
			stringBuilder.AppendLine("#   forceMapPositionsExemptAdmins - if true, moderators keep their own");
			stringBuilder.AppendLine("#                                   toggle and can stay hidden. Default false.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Discord (two independent channels - either, both, or neither):");
			stringBuilder.AppendLine("#   discordWebhookUrl       - PUBLIC channel. Receives only player-facing");
			stringBuilder.AppendLine("#                             events (joined / kicked / banned / died) in plain");
			stringBuilder.AppendLine("#                             language. Safe for community-visible channels.");
			stringBuilder.AppendLine("#   discordWebhookUrlAdmin  - ADMIN channel. Receives CURATED admin-relevant");
			stringBuilder.AppendLine("#                             events: violation strikes, config reloads, admin");
			stringBuilder.AppendLine("#                             command audit, kicks/bans, daily summary. Clean");
			stringBuilder.AppendLine("#                             enough to scan; use a moderator-only channel.");
			stringBuilder.AppendLine("#   discordVerboseMirror    - if true, ALSO mirror every ServerGuard log line");
			stringBuilder.AppendLine("#                             to the admin channel (noisy). Default false.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Daily summary:");
			stringBuilder.AppendLine("#   dailySummaryEnabled     - if true, post a one-paragraph digest each day.");
			stringBuilder.AppendLine("#   dailySummaryHourUtc     - 0..23, UTC hour at which the post fires.");
			stringBuilder.AppendLine("#   dailySummaryChannel     - 'public' | 'admin' | 'both'.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Per-rule violation accounting (countAsViolation):");
			stringBuilder.AppendLine("#   Each rule can independently decide whether a failure increments the");
			stringBuilder.AppendLine("#   player's violation count toward auto-ban. A 'false' rule still kicks the");
			stringBuilder.AppendLine("#   player (when enforce: true) but doesn't add a strike. Tune to match how");
			stringBuilder.AppendLine("#   strict you want your server to be. Defaults shown below.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Privilege tiers (two separate files, no setting needed):");
			stringBuilder.AppendLine("#   conf/owners.yaml  - OWNER. Exempt from every rule in this mod,");
			stringBuilder.AppendLine("#                       unconditionally. Cannot be kicked or banned by");
			stringBuilder.AppendLine("#                       ServerGuard. Keep this list to yourself.");
			stringBuilder.AppendLine("#   conf/moderators.yaml");
			stringBuilder.AppendLine("#                     - MODERATOR (staff). Runs `sg` commands and skips the");
			stringBuilder.AppendLine("#                       attestation, devcommand, speed, character-limit and");
			stringBuilder.AppendLine("#                       console checks. Still subject to the ban layer.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Ban layer:");
			stringBuilder.AppendLine("#   enableBanLayer          - independent SteamID denylist held in conf/bans.yaml.");
			stringBuilder.AppendLine("#                             Enforced inside the Valheim handshake (ZNet.IsAllowed)");
			stringBuilder.AppendLine("#                             instead of on the 5-second sweep vanilla uses, so a");
			stringBuilder.AppendLine("#                             banned player is refused before they can spawn.");
			stringBuilder.AppendLine("#   banLayerKickMessage     - text shown to a refused player.");
			stringBuilder.AppendLine("#   banLayerMirrorToVanilla - also write bans into Valheim's own banlist.txt.");
			stringBuilder.AppendLine("#                             Manage with: sg ban / sg unban / sg bans");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Console guard:");
			stringBuilder.AppendLine("#   consoleGuardMode        - 'open'       no gating");
			stringBuilder.AppendLine("#                             'restricted' block risky + cheat-flagged commands (default)");
			stringBuilder.AppendLine("#                             'whitelist'  block everything except consoleAllowedCommands");
			stringBuilder.AppendLine("#                             'disabled'   the F5 console cannot be opened at all");
			stringBuilder.AppendLine("#   consoleGuardExemptModerators");
			stringBuilder.AppendLine("#                           - moderators (moderators.yaml) keep full console access.");
			stringBuilder.AppendLine("#                             Note: `sg` commands are typed in the console, so");
			stringBuilder.AppendLine("#                             setting this false under 'disabled' removes the sg");
			stringBuilder.AppendLine("#                             interface for moderators too.");
			stringBuilder.AppendLine("#                             Owners (owners.yaml) are ALWAYS exempt - this");
			stringBuilder.AppendLine("#                             setting does not apply to them.");
			stringBuilder.AppendLine("#   consoleGuardBindPolicy  - 'allow' | 'block' | 'purge' (default) | 'wipe'.");
			stringBuilder.AppendLine("#                             Key binds are the sharpest edge in the console:");
			stringBuilder.AppendLine("#                             Valheim runs them with the context check skipped and");
			stringBuilder.AppendLine("#                             persists them client-side, so a bind made offline");
			stringBuilder.AppendLine("#                             still fires on your server. 'purge' clears them for");
			stringBuilder.AppendLine("#                             the session; 'wipe' also erases them from disk.");
			stringBuilder.AppendLine("#   consoleBlockedCommands  - extra command names to block (mode: restricted).");
			stringBuilder.AppendLine("#   consoleAllowedCommands  - the permitted set (mode: whitelist).");
			stringBuilder.AppendLine("#   consoleGuardReportAttempts - log/post/count blocked attempts.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine(_yamlOutFull.Serialize((object)settings));
			File.WriteAllText(SettingsYaml, stringBuilder.ToString());
		}
		MigrateAdminsToModerators();
		if (!File.Exists(ModeratorsYaml))
		{
			WriteModeratorsFile(new List<string>());
		}
		if (!File.Exists(OwnersYaml))
		{
			StringBuilder stringBuilder2 = new StringBuilder();
			stringBuilder2.AppendLine("# OWNER list: one SteamID64 per entry. Normally just you.");
			stringBuilder2.AppendLine("#");
			stringBuilder2.AppendLine("# An owner is exempt from EVERY rule in this mod, unconditionally. There is no");
			stringBuilder2.AppendLine("# setting to make a rule apply to an owner. Specifically, an owner:");
			stringBuilder2.AppendLine("#   - is never kicked or banned by ServerGuard, and an entry in bans.yaml");
			stringBuilder2.AppendLine("#     matching an owner is ignored");
			stringBuilder2.AppendLine("#   - never accrues violation strikes, so can never hit the auto-ban threshold");
			stringBuilder2.AppendLine("#   - skips the mod-manifest attestation entirely");
			stringBuilder2.AppendLine("#   - is never speed-checked, skill-capped or animation-cancel checked");
			stringBuilder2.AppendLine("#   - is never subject to the character limit or cheat-item removal");
			stringBuilder2.AppendLine("#   - has full console access regardless of consoleGuardMode, and keeps its");
			stringBuilder2.AppendLine("#     key binds regardless of consoleGuardBindPolicy");
			stringBuilder2.AppendLine("#   - is exempt from forced map positions");
			stringBuilder2.AppendLine("#   - has full `sg` command access");
			stringBuilder2.AppendLine("#");
			stringBuilder2.AppendLine("# Owners do NOT need to be listed in moderators.yaml as well - the owner tier");
			stringBuilder2.AppendLine("# already includes everything moderators can do.");
			stringBuilder2.AppendLine("#");
			stringBuilder2.AppendLine("# Keep this list as short as it can possibly be. Anyone here is invisible to");
			stringBuilder2.AppendLine("# every check the mod performs.");
			stringBuilder2.AppendLine("#");
			stringBuilder2.AppendLine("# TO ADD YOURSELF: delete the '#' on the line below and put your SteamID64 in.");
			stringBuilder2.AppendLine();
			stringBuilder2.AppendLine("owners:");
			stringBuilder2.AppendLine("#  - \"76561198000000000\"");
			File.WriteAllText(OwnersYaml, stringBuilder2.ToString());
		}
		if (!File.Exists(BansYaml))
		{
			SaveBans();
		}
		TryRenameLegacy(LegacyIgnoreModsYaml, LegacyIgnoreModsYaml + ".legacy");
		TryRenameLegacy(LegacyModPatternsYaml, LegacyModPatternsYaml + ".legacy");
		if (!File.Exists(AllowedModsYaml))
		{
			StringBuilder stringBuilder3 = new StringBuilder();
			stringBuilder3.AppendLine("# ServerGuard allowed_mods.yaml (v1.3+)");
			stringBuilder3.AppendLine("#");
			stringBuilder3.AppendLine("# Each entry references a mod by its BepInEx plugin GUID (preferred) or display Name.");
			stringBuilder3.AppendLine("# Optional `|<sha256_hex>` suffix pins the DLL hash; mismatch -> kick.");
			stringBuilder3.AppendLine("#");
			stringBuilder3.AppendLine("#   required_mods: every connecting client MUST report all of these in its manifest.");
			stringBuilder3.AppendLine("#   allowed_mods : extra mods the client may run beyond the required set.");
			stringBuilder3.AppendLine("#   banned_mods  : if any of these appear in the client manifest, the client is kicked.");
			stringBuilder3.AppendLine("#");
			stringBuilder3.AppendLine("# Recommended workflow:");
			stringBuilder3.AppendLine("#   1. Install the ServerGuard companion plugin on a client that has your full modpack.");
			stringBuilder3.AppendLine("#   2. Launch Valheim once. The client writes a snippet to:");
			stringBuilder3.AppendLine("#        <profile>/BepInEx/config/ServerGuard/mods_for_allowed_mods.yaml");
			stringBuilder3.AppendLine("#   3. Paste that snippet's `allowed_mods:` block into this file.");
			stringBuilder3.AppendLine("#");
			stringBuilder3.AppendLine("# Or for ad-hoc harvesting: set logPeerManifest: true in settings.yaml and connect a");
			stringBuilder3.AppendLine("# real client - every GUID will appear in BepInEx/LogOutput.log.");
			stringBuilder3.AppendLine();
			stringBuilder3.AppendLine("required_mods:");
			stringBuilder3.AppendLine("  - com.taeguk.valheim.serverguard.client    # the ServerGuard companion plugin");
			stringBuilder3.AppendLine();
			stringBuilder3.AppendLine("allowed_mods: []");
			stringBuilder3.AppendLine();
			stringBuilder3.AppendLine("banned_mods: []");
			stringBuilder3.AppendLine();
			File.WriteAllText(AllowedModsYaml, stringBuilder3.ToString());
		}
		if (!File.Exists(RegistrationsYaml))
		{
			RegistrationsDoc registrationsDoc = new RegistrationsDoc();
			File.WriteAllText(RegistrationsYaml, _yamlOut.Serialize((object)registrationsDoc));
		}
		if (!File.Exists(ViolationsYaml))
		{
			ViolationsDoc violationsDoc = new ViolationsDoc();
			File.WriteAllText(ViolationsYaml, _yamlOut.Serialize((object)violationsDoc));
		}
		if (!File.Exists(MetricsYaml))
		{
			DetectionMetrics detectionMetrics = new DetectionMetrics();
			StringBuilder stringBuilder4 = new StringBuilder();
			stringBuilder4.AppendLine("# ServerGuard Detection Metrics (auto-updated)");
			stringBuilder4.AppendLine(_yamlOut.Serialize((object)detectionMetrics));
			File.WriteAllText(MetricsYaml, stringBuilder4.ToString());
		}
	}

	private static void TryRenameLegacy(string from, string to)
	{
		try
		{
			if (File.Exists(from))
			{
				if (File.Exists(to))
				{
					File.Delete(to);
				}
				File.Move(from, to);
				ManualLogSource logS = LogS;
				if (logS != null)
				{
					logS.LogWarning((object)("[ServerGuard] Renamed legacy config '" + Path.GetFileName(from) + "' -> '" + Path.GetFileName(to) + "'. The new client-attestation flow uses allowed_mods.yaml."));
				}
			}
		}
		catch (Exception ex)
		{
			ManualLogSource logS2 = LogS;
			if (logS2 != null)
			{
				logS2.LogWarning((object)("[ServerGuard] Could not rename legacy file '" + from + "': " + ex.Message));
			}
		}
	}

	private void LoadSettings()
	{
		try
		{
			string text = File.ReadAllText(SettingsYaml);
			_settings = _yamlIn.Deserialize<Settings>(text) ?? new Settings();
			if (string.IsNullOrWhiteSpace(_settings.discordWebhookUrlAdmin))
			{
				string text2 = ReadScalarKey(text, "discordAdminWebhookUrl");
				if (!string.IsNullOrWhiteSpace(text2))
				{
					_settings.discordWebhookUrlAdmin = text2;
					LogS.LogWarning((object)"[ServerGuard] settings.yaml uses the legacy key 'discordAdminWebhookUrl'. Honouring it, but rename it to 'discordWebhookUrlAdmin' - the legacy spelling may go away.");
				}
			}
			if (_settings.CountAsViolation != null && _settings.CountAsViolation.Comparer != StringComparer.OrdinalIgnoreCase)
			{
				Dictionary<string, bool> dictionary = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
				foreach (KeyValuePair<string, bool> item in _settings.CountAsViolation)
				{
					dictionary[item.Key] = item.Value;
				}
				_settings.CountAsViolation = dictionary;
			}
			if (_settings.RequireHmac && string.IsNullOrWhiteSpace(_settings.SharedSecret))
			{
				_settings.SharedSecret = GenerateSharedSecret();
				try
				{
					PersistSharedSecret(_settings.SharedSecret);
					LogS.LogWarning((object)"[ServerGuard] sharedSecret was empty - generated a new one and wrote it back to settings.yaml. Copy this value into every client's client.yaml:");
					LogS.LogWarning((object)("[ServerGuard] sharedSecret: " + _settings.SharedSecret));
				}
				catch (Exception ex)
				{
					LogS.LogError((object)("[ServerGuard] Failed to persist generated sharedSecret: " + ex.Message + ". Generated value (use this in client.yaml): " + _settings.SharedSecret));
				}
			}
			LogS.LogInfo((object)"[ServerGuard] settings.yaml loaded");
			if (_bootCompleted)
			{
				PostAdminEvent(":arrows_counterclockwise: settings.yaml reloaded");
				BroadcastArrivalShoutPolicy();
				BroadcastConsolePolicy();
				SweepBannedPeers();
			}
		}
		catch (Exception ex2)
		{
			LogS.LogError((object)("[ServerGuard] Failed to load settings.yaml: " + ex2.Message));
			_settings = new Settings();
		}
		try
		{
			ReconfigureDiscordAndSummary();
		}
		catch (Exception ex3)
		{
			ManualLogSource logS = LogS;
			if (logS != null)
			{
				logS.LogWarning((object)("[ServerGuard] Discord/summary reconfigure failed: " + ex3.Message));
			}
		}
	}

	private static string ReadScalarKey(string yamlText, string key)
	{
		if (string.IsNullOrEmpty(yamlText) || string.IsNullOrEmpty(key))
		{
			return null;
		}
		string[] array = yamlText.Split(new char[1] { '\n' });
		for (int i = 0; i < array.Length; i++)
		{
			string text = array[i].TrimEnd(new char[1] { '\r' });
			if (text.Length == 0 || char.IsWhiteSpace(text[0]) || text.TrimStart(Array.Empty<char>()).StartsWith("#"))
			{
				continue;
			}
			int num = text.IndexOf(':');
			if (num <= 0 || !string.Equals(text.Substring(0, num).Trim(), key, StringComparison.Ordinal))
			{
				continue;
			}
			string text2 = text.Substring(num + 1).Trim();
			if (text2.Length > 0 && text2[0] != '"' && text2[0] != '\'')
			{
				int num2 = text2.IndexOf('#');
				if (num2 >= 0)
				{
					text2 = text2.Substring(0, num2).Trim();
				}
			}
			if (text2.Length >= 2 && ((text2[0] == '"' && text2[text2.Length - 1] == '"') || (text2[0] == '\'' && text2[text2.Length - 1] == '\'')))
			{
				text2 = text2.Substring(1, text2.Length - 2);
			}
			return text2;
		}
		return null;
	}

	private void TopUpSettingsFile()
	{
		try
		{
			if (_settings == null || !File.Exists(SettingsYaml))
			{
				return;
			}
			string text = File.ReadAllText(SettingsYaml);
			HashSet<string> present = new HashSet<string>(StringComparer.Ordinal);
			string[] array = text.Split(new char[1] { '\n' });
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i].TrimEnd(new char[1] { '\r' });
				if (text2.Length == 0 || char.IsWhiteSpace(text2[0]))
				{
					continue;
				}
				string text3 = text2.TrimStart(Array.Empty<char>());
				if (!text3.StartsWith("#") && !text3.StartsWith("-"))
				{
					int num = text2.IndexOf(':');
					if (num > 0)
					{
						present.Add(text2.Substring(0, num).Trim());
					}
				}
			}
			string text4 = _yamlOutFull.Serialize((object)_settings);
			List<KeyValuePair<string, List<string>>> list = new List<KeyValuePair<string, List<string>>>();
			List<string> list2 = null;
			array = text4.Split(new char[1] { '\n' });
			for (int i = 0; i < array.Length; i++)
			{
				string text5 = array[i].TrimEnd(new char[1] { '\r' });
				if (text5.Length == 0)
				{
					list2?.Add(text5);
					continue;
				}
				if (!char.IsWhiteSpace(text5[0]) && !text5.TrimStart(Array.Empty<char>()).StartsWith("-"))
				{
					int num2 = text5.IndexOf(':');
					if (num2 > 0)
					{
						list2 = new List<string>();
						list.Add(new KeyValuePair<string, List<string>>(text5.Substring(0, num2).Trim(), list2));
					}
				}
				list2?.Add(text5);
			}
			List<KeyValuePair<string, List<string>>> list3 = list.Where((KeyValuePair<string, List<string>> b) => !present.Contains(b.Key)).ToList();
			if (list3.Count == 0)
			{
				return;
			}
			StringBuilder stringBuilder = new StringBuilder();
			if (!text.EndsWith("\n"))
			{
				stringBuilder.AppendLine();
			}
			stringBuilder.AppendLine();
			stringBuilder.AppendLine("# ---------------------------------------------------------------------------");
			stringBuilder.AppendLine($"# Settings added automatically on {DateTime.UtcNow:yyyy-MM-dd} because they were missing");
			stringBuilder.AppendLine("# from this file. The values below are the ones already in effect, so writing");
			stringBuilder.AppendLine("# them changes nothing - they are here so you can see and edit them.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# (Older builds generated settings.yaml with defaults omitted, which hid every");
			stringBuilder.AppendLine("#  option that defaults to false, 0 or an empty list.)");
			stringBuilder.AppendLine("# ---------------------------------------------------------------------------");
			foreach (KeyValuePair<string, List<string>> item in list3)
			{
				foreach (string item2 in item.Value)
				{
					if (item2.Length != 0)
					{
						stringBuilder.AppendLine(item2);
					}
				}
			}
			File.AppendAllText(SettingsYaml, stringBuilder.ToString());
			LogS.LogWarning((object)($"[ServerGuard] settings.yaml was missing {list3.Count} option(s) - appended them with their current values: " + string.Join(", ", list3.Select((KeyValuePair<string, List<string>> b) => b.Key))));
		}
		catch (Exception ex)
		{
			ManualLogSource logS = LogS;
			if (logS != null)
			{
				logS.LogWarning((object)("[ServerGuard] Could not top up settings.yaml: " + ex.Message));
			}
		}
	}

	private void ReconfigureDiscordAndSummary()
	{
		if (_settings == null)
		{
			return;
		}
		string value = _settings.discordWebhookUrl ?? "";
		string text = _settings.discordWebhookUrlAdmin ?? "";
		string.IsNullOrWhiteSpace(value);
		bool flag = !string.IsNullOrWhiteSpace(text) && _settings.DiscordVerboseMirror;
		bool flag2 = _discordListener != null;
		bool flag3 = !string.Equals(text, _attachedAdminWebhookUrl, StringComparison.Ordinal);
		if (flag2 && (!flag || flag3))
		{
			try
			{
				Logger.Listeners.Remove((ILogListener)(object)_discordListener);
			}
			catch
			{
			}
			try
			{
				_discordListener.Dispose();
			}
			catch
			{
			}
			_discordListener = null;
			flag2 = false;
		}
		if (flag && !flag2)
		{
			try
			{
				ManualLogSource logS = LogS;
				string text2 = ((logS != null) ? logS.SourceName : null) ?? "Valheim ServerGuard";
				_discordListener = new DiscordLogListener(text, "[ServerGuard]", text2);
				Logger.Listeners.Add((ILogListener)(object)_discordListener);
				LogS.LogInfo((object)("[ServerGuard] Admin Discord verbose mirror enabled for source '" + text2 + "'."));
			}
			catch (Exception ex)
			{
				LogS.LogWarning((object)("[ServerGuard] Failed to enable admin Discord verbose mirror: " + ex.Message));
			}
		}
		if (flag3)
		{
			if (!string.IsNullOrWhiteSpace(text))
			{
				LogS.LogInfo((object)("[ServerGuard] Admin Discord channel armed (curated events; verbose mirror: " + (_settings.DiscordVerboseMirror ? "ON" : "OFF") + ")."));
			}
			else
			{
				LogS.LogInfo((object)"[ServerGuard] Admin Discord channel disabled (URL not set).");
			}
			_attachedAdminWebhookUrl = text;
		}
		if (!_dailySummaryStarted && _settings.DailySummaryEnabled && (!string.IsNullOrWhiteSpace(value) || !string.IsNullOrWhiteSpace(text)))
		{
			((MonoBehaviour)this).StartCoroutine(DailySummaryLoop());
			_dailySummaryStarted = true;
			LogS.LogInfo((object)$"[ServerGuard] Daily summary enabled (fires at {_settings.DailySummaryHourUtc:D2}:00 UTC, channel: {_settings.DailySummaryChannel}).");
		}
	}

	private void LoadAdmins()
	{
		try
		{
			if (!File.Exists(ModeratorsYaml))
			{
				_admins = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
				LogS.LogWarning((object)"[ServerGuard] moderators.yaml not present - no moderators configured.");
				return;
			}
			string text = File.ReadAllText(ModeratorsYaml);
			ModeratorsDoc obj = _yamlIn.Deserialize<ModeratorsDoc>(text) ?? new ModeratorsDoc();
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			int num = 0;
			foreach (string item in obj.All())
			{
				string text2 = (item ?? "").Trim();
				if (!IsValidSteamId(text2))
				{
					string text3 = ExtractSteamIdFromString(text2);
					if (IsValidSteamId(text3))
					{
						text2 = text3;
					}
				}
				if (!IsValidSteamId(text2))
				{
					num++;
				}
				else
				{
					hashSet.Add(text2);
				}
			}
			_admins = hashSet;
			LogS.LogInfo((object)($"[ServerGuard] moderators.yaml loaded ({_admins.Count} moderator(s)" + ((num > 0) ? $", {num} invalid" : "") + ")"));
			WarnIfIdsOnlyInComments(ModeratorsYaml, "moderators.yaml", _admins.Count);
			if (_bootCompleted)
			{
				PostAdminEvent($":arrows_counterclockwise: moderators.yaml reloaded ({_admins.Count} moderator(s))");
				BroadcastConsolePolicy();
			}
		}
		catch (Exception ex)
		{
			LogS.LogError((object)("[ServerGuard] Failed to load moderators.yaml: " + ex.Message));
			_admins = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
		}
	}

	private void WarnIfIdsOnlyInComments(string path, string label, int loadedCount)
	{
		try
		{
			if (loadedCount > 0 || !File.Exists(path))
			{
				return;
			}
			string[] array = File.ReadAllLines(path);
			for (int i = 0; i < array.Length; i++)
			{
				string text = array[i].Trim();
				if (text.StartsWith("#"))
				{
					string text2 = ExtractSteamIdFromString(text);
					if (IsValidSteamId(text2) && !(text2 == "76561198000000000"))
					{
						LogS.LogWarning((object)("[ServerGuard] " + label + " has no entries, but a COMMENTED line contains SteamID " + text2 + ". Remove the leading '#' for it to take effect - as written it does nothing."));
						break;
					}
				}
			}
		}
		catch
		{
		}
	}

	private void MigrateAdminsToModerators()
	{
		try
		{
			if (!File.Exists(LegacyAdminsYaml))
			{
				return;
			}
			if (File.Exists(ModeratorsYaml))
			{
				TryRenameLegacy(LegacyAdminsYaml, LegacyAdminsYaml + ".legacy");
				return;
			}
			List<string> list = new List<string>();
			try
			{
				foreach (string item in (_yamlIn.Deserialize<ModeratorsDoc>(File.ReadAllText(LegacyAdminsYaml)) ?? new ModeratorsDoc()).All())
				{
					string text = (item ?? "").Trim();
					if (!IsValidSteamId(text))
					{
						string text2 = ExtractSteamIdFromString(text);
						if (IsValidSteamId(text2))
						{
							text = text2;
						}
					}
					if (IsValidSteamId(text) && !list.Contains(text))
					{
						list.Add(text);
					}
				}
			}
			catch (Exception ex)
			{
				LogS.LogError((object)("[ServerGuard] Could not parse admins.yaml for migration (" + ex.Message + "). Left it in place - copy your SteamIDs into moderators.yaml manually."));
				return;
			}
			WriteModeratorsFile(list);
			TryRenameLegacy(LegacyAdminsYaml, LegacyAdminsYaml + ".legacy");
			LogS.LogWarning((object)($"[ServerGuard] Migrated admins.yaml -> moderators.yaml ({list.Count} moderator(s)). " + "The old file is now admins.yaml.legacy and is no longer read. The admin tier was renamed to MODERATOR in 1.7.0; the new owners.yaml is the tier above it."));
		}
		catch (Exception ex2)
		{
			LogS.LogError((object)("[ServerGuard] admins.yaml migration failed: " + ex2.Message));
		}
	}

	private void WriteModeratorsFile(List<string> ids)
	{
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.AppendLine("# MODERATOR list: one SteamID64 per entry.");
		stringBuilder.AppendLine("#");
		stringBuilder.AppendLine("# Moderators are staff. They can run `sg` commands and are exempt from the");
		stringBuilder.AppendLine("# attestation handshake, the devcommand gate, the console guard, the speed check");
		stringBuilder.AppendLine("# and the character limit.");
		stringBuilder.AppendLine("#");
		stringBuilder.AppendLine("# They are NOT exempt from the ban layer and can still be kicked. The tier that");
		stringBuilder.AppendLine("# bypasses everything is conf/owners.yaml.");
		stringBuilder.AppendLine("#");
		stringBuilder.AppendLine("# Renamed from admins.yaml in 1.7.0. An `admins:` key is still read here for");
		stringBuilder.AppendLine("# convenience if you paste an old file in.");
		stringBuilder.AppendLine();
		if (ids == null || ids.Count == 0)
		{
			stringBuilder.AppendLine("# TO ADD SOMEONE: delete the '#' on the line below and put their SteamID64 in.");
			stringBuilder.AppendLine();
			stringBuilder.AppendLine("moderators:");
			stringBuilder.AppendLine("#  - \"76561198000000000\"");
		}
		else
		{
			stringBuilder.AppendLine("moderators:");
			foreach (string id in ids)
			{
				stringBuilder.AppendLine("  - \"" + YamlEscape(id) + "\"");
			}
		}
		File.WriteAllText(ModeratorsYaml, stringBuilder.ToString());
	}

	private void LoadOwners()
	{
		try
		{
			if (!File.Exists(OwnersYaml))
			{
				_owners = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
				LogS.LogInfo((object)"[ServerGuard] owners.yaml not present - no owner configured.");
				return;
			}
			OwnersDoc obj = _yamlIn.Deserialize<OwnersDoc>(File.ReadAllText(OwnersYaml)) ?? new OwnersDoc();
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			int num = 0;
			foreach (string item in obj.owners ?? new List<string>())
			{
				string text = (item ?? "").Trim();
				if (!IsValidSteamId(text))
				{
					string text2 = ExtractSteamIdFromString(text);
					if (IsValidSteamId(text2))
					{
						text = text2;
					}
				}
				if (!IsValidSteamId(text))
				{
					num++;
				}
				else
				{
					hashSet.Add(text);
				}
			}
			_owners = hashSet;
			LogS.LogInfo((object)($"[ServerGuard] owners.yaml loaded ({_owners.Count} owner(s)" + ((num > 0) ? $", {num} invalid" : "") + ")"));
			WarnIfIdsOnlyInComments(OwnersYaml, "owners.yaml", _owners.Count);
			if (_bootCompleted)
			{
				PostAdminEvent($":crown: owners.yaml reloaded ({_owners.Count} owner(s))");
				BroadcastConsolePolicy();
			}
		}
		catch (Exception ex)
		{
			LogS.LogError((object)("[ServerGuard] Failed to load owners.yaml: " + ex.Message + ". No owner is configured until this is fixed."));
			_owners = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
		}
	}

	private void LoadBans()
	{
		try
		{
			if (!File.Exists(BansYaml))
			{
				lock (_bansLock)
				{
					_bans = new Dictionary<string, BanEntry>(StringComparer.OrdinalIgnoreCase);
				}
				LogS.LogInfo((object)"[ServerGuard] bans.yaml not present - ban layer has no entries.");
				return;
			}
			BansDoc obj = _yamlIn.Deserialize<BansDoc>(File.ReadAllText(BansYaml)) ?? new BansDoc();
			Dictionary<string, BanEntry> dictionary = new Dictionary<string, BanEntry>(StringComparer.OrdinalIgnoreCase);
			DateTime utcNow = DateTime.UtcNow;
			int num = 0;
			int num2 = 0;
			foreach (BanEntry item in obj.bans ?? new List<BanEntry>())
			{
				if (item == null)
				{
					continue;
				}
				string text = (item.id ?? "").Trim();
				if (!IsValidSteamId(text))
				{
					string text2 = ExtractSteamIdFromString(text);
					if (IsValidSteamId(text2))
					{
						text = text2;
					}
				}
				if (!IsValidSteamId(text))
				{
					num++;
					continue;
				}
				if (item.IsExpired(utcNow))
				{
					num2++;
					continue;
				}
				item.id = text;
				dictionary[text] = item;
			}
			lock (_bansLock)
			{
				_bans = dictionary;
			}
			LogS.LogInfo((object)($"[ServerGuard] bans.yaml loaded ({dictionary.Count} active" + ((num2 > 0) ? $", {num2} expired" : "") + ((num > 0) ? $", {num} invalid" : "") + ")"));
			if (_bootCompleted)
			{
				PostAdminEvent($":arrows_counterclockwise: bans.yaml reloaded ({dictionary.Count} active ban(s))");
				SweepBannedPeers();
			}
		}
		catch (Exception ex)
		{
			LogS.LogError((object)("[ServerGuard] Failed to load bans.yaml: " + ex.Message));
			if (_bootCompleted)
			{
				PostAdminEvent(":x: bans.yaml failed to parse - ban layer is running on the last good list. " + ex.Message);
			}
		}
	}

	private void SaveBans()
	{
		try
		{
			List<BanEntry> list;
			lock (_bansLock)
			{
				DateTime now = DateTime.UtcNow;
				list = _bans.Values.Where((BanEntry e) => e != null && !e.IsExpired(now)).OrderBy<BanEntry, string>((BanEntry e) => e.id, StringComparer.Ordinal).ToList();
				_bans = list.ToDictionary<BanEntry, string, BanEntry>((BanEntry e) => e.id, (BanEntry e) => e, StringComparer.OrdinalIgnoreCase);
			}
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("# ServerGuard ban list.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Enforced at the earliest point of the Valheim handshake at which the SteamID is");
			stringBuilder.AppendLine("# known, so a banned player's connection is refused before they ever spawn. This is");
			stringBuilder.AppendLine("# separate from Valheim's own banlist.txt - an in-game `unban` does NOT clear an");
			stringBuilder.AppendLine("# entry here. Use `sg unban <steamid>` or delete the line below.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Hot-reloaded: edits take effect within a second, and anyone already online who");
			stringBuilder.AppendLine("# matches a new entry is disconnected immediately.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Fields: id (required, SteamID64), reason, expires (ISO-8601 UTC, empty =");
			stringBuilder.AppendLine("# permanent), added, addedBy. Expired entries are removed on the next write.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# bans:");
			stringBuilder.AppendLine("#   - id: \"76561198000000000\"");
			stringBuilder.AppendLine("#     reason: \"Item duping\"");
			stringBuilder.AppendLine("#     expires: \"\"");
			stringBuilder.AppendLine();
			if (list.Count == 0)
			{
				stringBuilder.AppendLine("bans:");
			}
			else
			{
				stringBuilder.AppendLine("bans:");
				foreach (BanEntry item in list)
				{
					stringBuilder.AppendLine("  - id: \"" + YamlEscape(item.id) + "\"");
					stringBuilder.AppendLine("    reason: \"" + YamlEscape(item.reason) + "\"");
					stringBuilder.AppendLine("    expires: \"" + YamlEscape(item.expires) + "\"");
					stringBuilder.AppendLine("    added: \"" + YamlEscape(item.added) + "\"");
					stringBuilder.AppendLine("    addedBy: \"" + YamlEscape(item.addedBy) + "\"");
				}
			}
			File.WriteAllText(BansYaml, stringBuilder.ToString());
		}
		catch (Exception ex)
		{
			LogS.LogError((object)("[ServerGuard] Failed to save bans.yaml: " + ex.Message));
		}
	}

	private static string YamlEscape(string s)
	{
		return (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", " ")
			.Replace("\n", " ");
	}

	internal bool IsBannedId(string candidate, out BanEntry entry)
	{
		entry = null;
		if (_settings == null || !_settings.EnableBanLayer)
		{
			return false;
		}
		if (string.IsNullOrWhiteSpace(candidate))
		{
			return false;
		}
		string text = candidate.Trim();
		if (!IsValidSteamId(text))
		{
			string text2 = ExtractSteamIdFromString(text);
			if (!IsValidSteamId(text2))
			{
				return false;
			}
			text = text2;
		}
		if (IsOwner(text))
		{
			return false;
		}
		BanEntry value;
		lock (_bansLock)
		{
			if (!_bans.TryGetValue(text, out value) || value == null)
			{
				return false;
			}
		}
		if (value.IsExpired(DateTime.UtcNow))
		{
			return false;
		}
		entry = value;
		return true;
	}

	internal BanEntry AddBan(string steamId, string reason, string addedBy, string expiresIso = "")
	{
		if (IsOwner(steamId))
		{
			LogS.LogWarning((object)("[ServerGuard] Refusing to ban " + FormatPlayer(steamId) + " - owner."));
			return null;
		}
		BanEntry banEntry = new BanEntry
		{
			id = steamId,
			reason = (string.IsNullOrWhiteSpace(reason) ? "No reason given." : reason.Trim()),
			expires = (expiresIso ?? ""),
			added = DateTime.UtcNow.ToString("o"),
			addedBy = (addedBy ?? "")
		};
		lock (_bansLock)
		{
			_bans[steamId] = banEntry;
		}
		SaveBans();
		if (_settings != null && _settings.BanLayerMirrorToVanilla)
		{
			TryVanillaBan(steamId);
		}
		return banEntry;
	}

	internal bool RemoveBan(string steamId)
	{
		bool flag;
		lock (_bansLock)
		{
			flag = _bans.Remove(steamId);
		}
		if (flag)
		{
			SaveBans();
		}
		return flag;
	}

	private void SweepBannedPeers()
	{
		try
		{
			if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || _settings == null || !_settings.EnableBanLayer)
			{
				return;
			}
			foreach (ZNetPeer item in ZNet.instance.GetPeers()?.ToList() ?? new List<ZNetPeer>())
			{
				if (item != null)
				{
					string peerPlatformId = GetPeerPlatformId(item);
					if (IsBannedId(peerPlatformId, out var entry))
					{
						LogS.LogWarning((object)("[ServerGuard] Ban sweep: disconnecting " + FormatPlayer(peerPlatformId) + " (" + entry.reason + ")"));
						PostPlayerEvent(":no_entry:", peerPlatformId, "was removed", "banned");
						TryKick(item, _settings.BanLayerKickMessage + " (" + entry.reason + ")");
					}
				}
			}
		}
		catch (Exception ex)
		{
			LogS.LogWarning((object)("[ServerGuard] Ban sweep error: " + ex.Message));
		}
	}

	private void TryVanillaBan(string platformId)
	{
		try
		{
			object obj = typeof(ZNet).GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null);
			obj?.GetType().GetMethod("Ban", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null)?.Invoke(obj, new object[1] { platformId });
		}
		catch (Exception ex)
		{
			LogS.LogWarning((object)("[ServerGuard] Vanilla ban mirror failed for " + platformId + ": " + ex.Message));
		}
	}

	private static string GetPeerSocketId(ZNetPeer peer)
	{
		try
		{
			object obj;
			if (peer == null)
			{
				obj = null;
			}
			else
			{
				ISocket socket = peer.m_socket;
				obj = ((socket != null) ? socket.GetHostName() : null);
			}
			string text = (string)obj;
			if (string.IsNullOrWhiteSpace(text))
			{
				return null;
			}
			if (IsValidSteamId(text))
			{
				return text;
			}
			string text2 = ExtractSteamIdFromString(text);
			return IsValidSteamId(text2) ? text2 : null;
		}
		catch
		{
			return null;
		}
	}

	private void LoadAllowedMods()
	{
		try
		{
			string text = File.ReadAllText(AllowedModsYaml);
			AllowedModsDoc allowedModsDoc = _yamlIn.Deserialize<AllowedModsDoc>(text) ?? new AllowedModsDoc();
			_requiredMods = ParseAllowedList(allowedModsDoc.required_mods);
			_allowedMods = ParseAllowedList(allowedModsDoc.allowed_mods);
			_bannedMods = ParseAllowedList(allowedModsDoc.banned_mods);
			LogS.LogInfo((object)$"[ServerGuard] allowed_mods.yaml loaded (required={_requiredMods.Count}, allowed={_allowedMods.Count}, banned={_bannedMods.Count})");
			if (_bootCompleted)
			{
				PostAdminEvent($":arrows_counterclockwise: allowed_mods.yaml reloaded — req={_requiredMods.Count} allow={_allowedMods.Count} ban={_bannedMods.Count}");
			}
		}
		catch (Exception ex)
		{
			LogS.LogError((object)("[ServerGuard] Failed to load allowed_mods.yaml: " + ex.Message));
			_requiredMods = new List<AllowedModEntry>();
			_allowedMods = new List<AllowedModEntry>();
			_bannedMods = new List<AllowedModEntry>();
		}
		RecomputeModsetFingerprint();
	}

	private void RecomputeModsetFingerprint()
	{
		try
		{
			List<KeyValuePair<string, string>> entries = (from e in _requiredMods.Concat(_allowedMods)
				select new KeyValuePair<string, string>(e?.Key ?? "", e?.Sha256 ?? "")).ToList();
			_modsetFingerprintStrict = ModsetFingerprint.ComputeStrict(entries);
			_modsetFingerprintLoose = ModsetFingerprint.ComputeLoose(entries);
			string path = Path.Combine(ConfDir, "modset_fingerprint.txt");
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("# Modset fingerprint for this server.");
			stringBuilder.AppendLine("# Re-generated on every hot reload of allowed_mods.yaml.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# LOOSE  - matches across version bumps. Useful for 'are we on the same modpack?'");
			stringBuilder.AppendLine("# STRICT - also pins each mod's DLL hash. Matches only on identical binaries.");
			stringBuilder.AppendLine("#");
			stringBuilder.AppendLine("# Players can compare these against their client startup log line:");
			stringBuilder.AppendLine("#   [ServerGuard.Client] Modset fingerprint  loose=XXXXXXXX  strict=YYYYYYYY");
			stringBuilder.AppendLine();
			stringBuilder.AppendLine("loose:  " + _modsetFingerprintLoose);
			stringBuilder.AppendLine("strict: " + _modsetFingerprintStrict);
			stringBuilder.AppendLine();
			stringBuilder.AppendLine("short_loose:  " + ModsetFingerprint.Short(_modsetFingerprintLoose));
			stringBuilder.AppendLine("short_strict: " + ModsetFingerprint.Short(_modsetFingerprintStrict));
			File.WriteAllText(path, stringBuilder.ToString());
			LogS.LogInfo((object)("[ServerGuard] Modset fingerprint  loose=" + ModsetFingerprint.Short(_modsetFingerprintLoose) + "  strict=" + ModsetFingerprint.Short(_modsetFingerprintStrict) + "  (full values in " + Path.GetFileName(path) + ")"));
		}
		catch (Exception ex)
		{
			LogS.LogWarning((object)("[ServerGuard] Failed to compute modset fingerprint: " + ex.Message));
		}
	}

	private static List<AllowedModEntry> ParseAllowedList(List<string> raw)
	{
		List<AllowedModEntry> list = new List<AllowedModEntry>();
		if (raw == null)
		{
			return list;
		}
		foreach (string item in raw)
		{
			if (!string.IsNullOrWhiteSpace(item))
			{
				string[] array = item.Split(new char[1] { '|' });
				string text = array[0].Trim();
				string sha = ((array.Length > 1) ? array[1].Trim().ToLowerInvariant() : null);
				if (!string.IsNullOrEmpty(text))
				{
					list.Add(new AllowedModEntry
					{
						Key = text.ToLowerInvariant(),
						Sha256 = sha
					});
				}
			}
		}
		return list;
	}

	private void LoadRegistrations()
	{
		try
		{
			string text = File.ReadAllText(RegistrationsYaml);
			RegistrationsDoc registrationsDoc = _yamlIn.Deserialize<RegistrationsDoc>(text);
			if (registrationsDoc?.registrations != null && registrationsDoc.registrations.Count > 0)
			{
				_registrations = registrationsDoc.registrations;
			}
			else
			{
				Dictionary<string, Dictionary<string, string>> dictionary = _yamlIn.Deserialize<Dictionary<string, Dictionary<string, string>>>(text);
				if (dictionary != null && dictionary.TryGetValue("registrations", out var value) && value != null)
				{
					Dictionary<string, List<string>> dictionary2 = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
					foreach (KeyValuePair<string, string> item in value)
					{
						if (!string.IsNullOrWhiteSpace(item.Key) && !string.IsNullOrWhiteSpace(item.Value))
						{
							dictionary2[item.Key] = new List<string> { item.Value.Trim() };
						}
					}
					_registrations = dictionary2;
					SaveRegistrations();
				}
				else
				{
					_registrations = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
				}
			}
			LogS.LogInfo((object)$"[ServerGuard] registrations.yaml loaded ({_registrations.Count} SteamIDs)");
		}
		catch (Exception ex)
		{
			LogS.LogError((object)("[ServerGuard] Failed to load registrations.yaml: " + ex.Message));
			_registrations = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
		}
	}

	private void LoadViolations()
	{
		try
		{
			string text = File.ReadAllText(ViolationsYaml);
			ViolationsDoc violationsDoc = _yamlIn.Deserialize<ViolationsDoc>(text) ?? new ViolationsDoc();
			_violations = violationsDoc.violations ?? new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase);
			LogS.LogInfo((object)$"[ServerGuard] violations.yaml loaded ({_violations.Count} players)");
		}
		catch (Exception ex)
		{
			LogS.LogError((object)("[ServerGuard] Failed to load violations.yaml: " + ex.Message));
			_violations = new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase);
		}
	}

	private void LoadMetrics()
	{
		try
		{
			string text = File.ReadAllText(MetricsYaml);
			_metrics = _yamlIn.Deserialize<DetectionMetrics>(text) ?? new DetectionMetrics();
			_metrics.last_updated = DateTime.UtcNow;
			LogS.LogInfo((object)$"[ServerGuard] metrics.yaml loaded (Checked: {_metrics.total_players_checked}, Detected: {_metrics.total_mods_detected})");
		}
		catch (Exception ex)
		{
			LogS.LogWarning((object)("[ServerGuard] Failed to load metrics.yaml: " + ex.Message));
			_metrics = new DetectionMetrics();
		}
	}

	private void SaveRegistrations()
	{
		RegistrationsDoc registrationsDoc = new RegistrationsDoc
		{
			registrations = _registrations
		};
		File.WriteAllText(RegistrationsYaml, _yamlOut.Serialize((object)registrationsDoc));
	}

	private void SaveViolations()
	{
		ViolationsDoc violationsDoc = new ViolationsDoc
		{
			violations = _violations
		};
		File.WriteAllText(ViolationsYaml, _yamlOut.Serialize((object)violationsDoc));
	}

	private void SaveMetrics()
	{
		try
		{
			if (_settings.EnableMetrics)
			{
				_metrics.last_updated = DateTime.UtcNow;
				DetectionMetrics metrics = _metrics;
				File.WriteAllText(MetricsYaml, _yamlOutFull.Serialize((object)metrics));
			}
		}
		catch (Exception ex)
		{
			LogS.LogWarning((object)("[ServerGuard] Failed to save metrics.yaml: " + ex.Message));
		}
	}

	private void SaveAll()
	{
		SaveRegistrations();
		SaveViolations();
		SaveMetrics();
	}

	private static string GetPeerPlatformId(object znetPeer)
	{
		try
		{
			FieldInfo field = znetPeer.GetType().GetField("m_platformUserID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (field != null && TryNormalizeSteamId(field.GetValue(znetPeer), out var normalized) && IsValidSteamId(normalized))
			{
				return normalized;
			}
			MethodInfo method = znetPeer.GetType().GetMethod("GetPlatformUserID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (method != null && TryNormalizeSteamId(method.Invoke(znetPeer, null), out var normalized2) && IsValidSteamId(normalized2))
			{
				return normalized2;
			}
			object obj = znetPeer.GetType().GetField("m_socket", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(znetPeer);
			if (obj != null)
			{
				FieldInfo field2 = obj.GetType().GetField("m_peerID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (field2 != null && TryNormalizeSteamId(field2.GetValue(obj), out var normalized3) && IsValidSteamId(normalized3))
				{
					return normalized3;
				}
				MethodInfo method2 = obj.GetType().GetMethod("GetPeerID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (method2 != null && TryNormalizeSteamId(method2.Invoke(obj, null), out var normalized4) && IsValidSteamId(normalized4))
				{
					return normalized4;
				}
				MethodInfo method3 = obj.GetType().GetMethod("GetSteamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (method3 != null && TryNormalizeSteamId(method3.Invoke(obj, null), out var normalized5) && IsValidSteamId(normalized5))
				{
					return normalized5;
				}
				MethodInfo method4 = obj.GetType().GetMethod("GetSteamID64", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (method4 != null && TryNormalizeSteamId(method4.Invoke(obj, null), out var normalized6) && IsValidSteamId(normalized6))
				{
					return normalized6;
				}
				PropertyInfo property = obj.GetType().GetProperty("SteamID", BindingFlags.Instance | BindingFla