Decompiled source of Valheim ServerGuard v1.4.0

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 Microsoft.CodeAnalysis;
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.3.0.0")]
[assembly: AssemblyInformationalVersion("1.3.0+aec5ff4e1c5c57d0d11bef9eaeebb37e9dabfb4d")]
[assembly: AssemblyProduct("Valheim-ServerGuard")]
[assembly: AssemblyTitle("Valheim-ServerGuard")]
[assembly: AssemblyVersion("1.3.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
[BepInPlugin("com.taeguk.valheim.serverguard", "Valheim ServerGuard", "1.4.0")]
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,
			["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 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 AdminsDoc
	{
		public List<string> admins { get; set; } = new List<string>();

	}

	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 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), "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 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_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);
				});
				if (Instance.IsAdmin(peerPlatformId))
				{
					LogS.LogInfo((object)("[ServerGuard] " + Instance.FormatPlayer(peerPlatformId) + " is admin - skipping attestation challenge."));
					if (Instance._settings.EnableMetrics)
					{
						Instance._metrics.admin_bypasses++;
						Instance.SaveMetrics();
					}
					Instance.PostPlayerEvent(":shield:", peerPlatformId, "joined as admin");
					return;
				}
				if (Instance._settings.EnableMetrics)
				{
					Instance._metrics.total_players_checked++;
					Instance.SaveMetrics();
				}
				string text = Instance.GenerateChallenge();
				Instance.RegisterPending(peer, peerPlatformId, text);
				peer.m_rpc.Invoke("ServerGuard_RequestManifest", new object[1] { text });
				((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)))
					{
						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}'");
						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}");
			}
		}
	}

	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;
				}
				LogLevel level = eventArgs.Level;
				string text = ((object)(LogLevel)(ref level)).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
			{
			}
		}
	}

	[CompilerGenerated]
	private sealed class <AttestationTimeoutCoroutine>d__134 : IEnumerator<object>, IDisposable, IEnumerator
	{
		private int <>1__state;

		private object <>2__current;

		public Plugin <>4__this;

		public ZNetPeer peer;

		public string steamId;

		private int <seconds>5__2;

		object IEnumerator<object>.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		object IEnumerator.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		[DebuggerHidden]
		public <AttestationTimeoutCoroutine>d__134(int <>1__state)
		{
			this.<>1__state = <>1__state;
		}

		[DebuggerHidden]
		void IDisposable.Dispose()
		{
			<>1__state = -2;
		}

		private bool MoveNext()
		{
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Expected O, but got Unknown
			int num = <>1__state;
			Plugin plugin = <>4__this;
			switch (num)
			{
			default:
				return false;
			case 0:
				<>1__state = -1;
				<seconds>5__2 = Mathf.Max(1, plugin._settings.CompanionTimeoutSeconds);
				<>2__current = (object)new WaitForSeconds((float)<seconds>5__2);
				<>1__state = 1;
				return true;
			case 1:
			{
				<>1__state = -1;
				lock (plugin._pendingLock)
				{
					if (!plugin._pending.TryGetValue(peer.m_uid, out var value) || value == null)
					{
						return false;
					}
					plugin._pending.Remove(peer.m_uid);
				}
				string arg = plugin.FormatPlayer(steamId);
				LogS.LogWarning((object)$"[ServerGuard] {arg} did not deliver a manifest within {<seconds>5__2}s. Treating as no-companion.");
				if (plugin._settings.RequireCompanion)
				{
					plugin.AddViolation(steamId, "CompanionMissing");
					if (plugin._settings.Enforce)
					{
						plugin.PostPlayerEvent(":door:", steamId, "was kicked", FriendlyReason("CompanionMissing"));
						plugin.TryKick(peer, plugin._settings.KickMessage + " (Missing required companion plugin: ServerGuard.Client)");
					}
				}
				return false;
			}
			}
		}

		bool IEnumerator.MoveNext()
		{
			//ILSpy generated this explicit interface implementation from .override directive in MoveNext
			return this.MoveNext();
		}

		[DebuggerHidden]
		void IEnumerator.Reset()
		{
			throw new NotSupportedException();
		}
	}

	[CompilerGenerated]
	private sealed class <BuildLogCleanupLoop>d__146 : IEnumerator<object>, IDisposable, IEnumerator
	{
		private int <>1__state;

		private object <>2__current;

		public Plugin <>4__this;

		object IEnumerator<object>.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		object IEnumerator.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		[DebuggerHidden]
		public <BuildLogCleanupLoop>d__146(int <>1__state)
		{
			this.<>1__state = <>1__state;
		}

		[DebuggerHidden]
		void IDisposable.Dispose()
		{
			<>1__state = -2;
		}

		private bool MoveNext()
		{
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Expected O, but got Unknown
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			//IL_007f: Expected O, but got Unknown
			int num = <>1__state;
			Plugin plugin = <>4__this;
			switch (num)
			{
			default:
				return false;
			case 0:
				<>1__state = -1;
				<>2__current = (object)new WaitForSeconds(300f);
				<>1__state = 1;
				return true;
			case 1:
				<>1__state = -1;
				break;
			case 2:
				<>1__state = -1;
				break;
			}
			try
			{
				plugin.PruneOldBuildLogs();
			}
			catch (Exception ex)
			{
				LogS.LogWarning((object)("[ServerGuard] PruneOldBuildLogs failed: " + ex.Message));
			}
			<>2__current = (object)new WaitForSeconds(3600f);
			<>1__state = 2;
			return true;
		}

		bool IEnumerator.MoveNext()
		{
			//ILSpy generated this explicit interface implementation from .override directive in MoveNext
			return this.MoveNext();
		}

		[DebuggerHidden]
		void IEnumerator.Reset()
		{
			throw new NotSupportedException();
		}
	}

	[CompilerGenerated]
	private sealed class <DailySummaryLoop>d__120 : IEnumerator<object>, IDisposable, IEnumerator
	{
		private int <>1__state;

		private object <>2__current;

		public Plugin <>4__this;

		object IEnumerator<object>.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		object IEnumerator.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		[DebuggerHidden]
		public <DailySummaryLoop>d__120(int <>1__state)
		{
			this.<>1__state = <>1__state;
		}

		[DebuggerHidden]
		void IDisposable.Dispose()
		{
			<>1__state = -2;
		}

		private bool MoveNext()
		{
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Expected O, but got Unknown
			//IL_008f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0099: Expected O, but got Unknown
			int num = <>1__state;
			Plugin plugin = <>4__this;
			switch (num)
			{
			default:
				return false;
			case 0:
			{
				<>1__state = -1;
				float num2 = (float)plugin.SecondsUntilNextFire();
				<>2__current = (object)new WaitForSeconds(num2);
				<>1__state = 1;
				return true;
			}
			case 1:
				<>1__state = -1;
				break;
			case 2:
				<>1__state = -1;
				break;
			}
			try
			{
				plugin.PostDailySummary(scheduled: true);
			}
			catch (Exception ex)
			{
				LogS.LogWarning((object)("[ServerGuard] Daily summary post failed: " + ex.Message));
			}
			float num3 = (float)plugin.SecondsUntilNextFire();
			if (num3 <= 60f)
			{
				num3 = 86400f;
			}
			<>2__current = (object)new WaitForSeconds(num3);
			<>1__state = 2;
			return true;
		}

		bool IEnumerator.MoveNext()
		{
			//ILSpy generated this explicit interface implementation from .override directive in MoveNext
			return this.MoveNext();
		}

		[DebuggerHidden]
		void IEnumerator.Reset()
		{
			throw new NotSupportedException();
		}
	}

	[CompilerGenerated]
	private sealed class <PingLogLoop>d__112 : IEnumerator<object>, IDisposable, IEnumerator
	{
		private int <>1__state;

		private object <>2__current;

		public Plugin <>4__this;

		object IEnumerator<object>.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		object IEnumerator.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		[DebuggerHidden]
		public <PingLogLoop>d__112(int <>1__state)
		{
			this.<>1__state = <>1__state;
		}

		[DebuggerHidden]
		void IDisposable.Dispose()
		{
			<>1__state = -2;
		}

		private bool MoveNext()
		{
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Expected O, but got Unknown
			//IL_0065: Unknown result type (might be due to invalid IL or missing references)
			//IL_006f: Expected O, but got Unknown
			int num = <>1__state;
			Plugin plugin = <>4__this;
			switch (num)
			{
			default:
				return false;
			case 0:
				<>1__state = -1;
				<>2__current = (object)new WaitForSeconds(15f);
				<>1__state = 1;
				return true;
			case 1:
				<>1__state = -1;
				break;
			case 2:
				<>1__state = -1;
				try
				{
					if (plugin._settings != null && plugin._settings.EnablePingLog && !((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
					{
						plugin.TickPingLog();
					}
				}
				catch (Exception ex)
				{
					LogS.LogWarning((object)("[ServerGuard] Ping log tick error: " + ex.Message));
				}
				break;
			}
			int num2 = Math.Max(2, plugin._settings?.PingLogSampleSeconds ?? 5);
			<>2__current = (object)new WaitForSeconds((float)num2);
			<>1__state = 2;
			return true;
		}

		bool IEnumerator.MoveNext()
		{
			//ILSpy generated this explicit interface implementation from .override directive in MoveNext
			return this.MoveNext();
		}

		[DebuggerHidden]
		void IEnumerator.Reset()
		{
			throw new NotSupportedException();
		}
	}

	[CompilerGenerated]
	private sealed class <SpeedCheckLoop>d__116 : IEnumerator<object>, IDisposable, IEnumerator
	{
		private int <>1__state;

		private object <>2__current;

		public Plugin <>4__this;

		object IEnumerator<object>.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		object IEnumerator.Current
		{
			[DebuggerHidden]
			get
			{
				return <>2__current;
			}
		}

		[DebuggerHidden]
		public <SpeedCheckLoop>d__116(int <>1__state)
		{
			this.<>1__state = <>1__state;
		}

		[DebuggerHidden]
		void IDisposable.Dispose()
		{
			<>1__state = -2;
		}

		private bool MoveNext()
		{
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Expected O, but got Unknown
			//IL_0062: Unknown result type (might be due to invalid IL or missing references)
			//IL_006c: Expected O, but got Unknown
			int num = <>1__state;
			Plugin plugin = <>4__this;
			switch (num)
			{
			default:
				return false;
			case 0:
				<>1__state = -1;
				<>2__current = (object)new WaitForSeconds(5f);
				<>1__state = 1;
				return true;
			case 1:
				<>1__state = -1;
				break;
			case 2:
				<>1__state = -1;
				try
				{
					if (plugin._settings.EnableSpeedCheck && !((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
					{
						plugin.TickSpeedCheck();
					}
				}
				catch (Exception ex)
				{
					LogS.LogWarning((object)("[ServerGuard] Speed check tick error: " + ex.Message));
				}
				break;
			}
			float num2 = Mathf.Max(0.25f, (float)plugin._settings.SpeedCheckSampleSeconds);
			<>2__current = (object)new WaitForSeconds(num2);
			<>1__state = 2;
			return true;
		}

		bool IEnumerator.MoveNext()
		{
			//ILSpy generated this explicit interface implementation from .override directive in MoveNext
			return this.MoveNext();
		}

		[DebuggerHidden]
		void IEnumerator.Reset()
		{
			throw new NotSupportedException();
		}
	}

	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 AdminsYaml = Path.Combine(ConfDir, "admins.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 Settings _settings;

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

	private DetectionMetrics _metrics;

	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_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[14]
	{
		"CompanionMissing", "HmacInvalid", "ChallengeMismatch", "RequiredModMissing", "DisallowedMod", "BannedMod", "HashMismatch", "CharacterNameLimitExceeded", "DevcommandAttempt", "SpeedHack",
		"IllegalItem", "StackOverflow", "AnimationCancel", "SkillOverflow"
	};

	private FileSystemWatcher _watchSettings;

	private FileSystemWatcher _watchAdmins;

	private FileSystemWatcher _watchAllowed;

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

	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 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_0084: Unknown result type (might be due to invalid IL or missing references)
		//IL_008e: 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();
		EnsureFoldersAndFiles();
		LoadSettings();
		LoadAdmins();
		LoadAllowedMods();
		LoadRegistrations();
		LoadViolations();
		LoadMetrics();
		StartWatchers();
		_harmony = new Harmony("com.taeguk.valheim.serverguard");
		_harmony.PatchAll();
		LogS.LogInfo((object)("[ServerGuard] Loaded (v1.4.0). 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}. " + "Metrics: " + (_settings.EnableMetrics ? "ON" : "OFF")));
		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.4.0  enforce=" + (_settings.Enforce ? "ON" : "off") + "  requireHmac=" + (_settings.RequireHmac ? "ON" : "off") + "  " + $"req/allow/ban={_requiredMods.Count}/{_allowedMods.Count}/{_bannedMods.Count}  " + "modset=`" + ModsetFingerprint.Short(_modsetFingerprintLoose) + "`");
		_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"));
		}
	}

	private void OnDestroy()
	{
		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.4.0)");
			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, sheathe) reported by the");
			stringBuilder.AppendLine("#                               companion 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("#");
			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("# 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(_yamlOut.Serialize((object)settings));
			File.WriteAllText(SettingsYaml, stringBuilder.ToString());
		}
		if (!File.Exists(AdminsYaml))
		{
			AdminsDoc adminsDoc = new AdminsDoc
			{
				admins = new List<string>()
			};
			StringBuilder stringBuilder2 = new StringBuilder();
			stringBuilder2.AppendLine("# Admin whitelist: one SteamID per entry");
			stringBuilder2.AppendLine(_yamlOut.Serialize((object)adminsDoc));
			File.WriteAllText(AdminsYaml, stringBuilder2.ToString());
		}
		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
		{
			_settings = _yamlIn.Deserialize<Settings>(File.ReadAllText(SettingsYaml)) ?? new Settings();
			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");
			}
		}
		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 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
		{
			string text = File.ReadAllText(AdminsYaml);
			AdminsDoc adminsDoc = _yamlIn.Deserialize<AdminsDoc>(text) ?? new AdminsDoc();
			_admins = new HashSet<string>(from s in adminsDoc.admins ?? new List<string>()
				select s.Trim() into s
				where !string.IsNullOrWhiteSpace(s)
				select s, StringComparer.OrdinalIgnoreCase);
			LogS.LogInfo((object)$"[ServerGuard] admins.yaml loaded ({_admins.Count} admins)");
			if (_bootCompleted)
			{
				PostAdminEvent($":arrows_counterclockwise: admins.yaml reloaded ({_admins.Count} admins)");
			}
		}
		catch (Exception ex)
		{
			LogS.LogError((object)("[ServerGuard] Failed to load admins.yaml: " + ex.Message));
			_admins = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
		}
	}

	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, _yamlOut.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 | BindingFlags.Public | BindingFlags.NonPublic);
				if (property != null && TryNormalizeSteamId(property.GetValue(obj, null), out var normalized7) && IsValidSteamId(normalized7))
				{
					return normalized7;
				}
				FieldInfo field3 = obj.GetType().GetField("m_SteamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (field3 != null && TryNormalizeSteamId(field3.GetValue(obj), out var normalized8) && IsValidSteamId(normalized8))
				{
					return normalized8;
				}
				FieldInfo field4 = obj.GetType().GetField("m_steamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (field4 != null && TryNormalizeSteamId(field4.GetValue(obj), out var normalized9) && IsValidSteamId(normalized9))
				{
					return normalized9;
				}
				MethodInfo method5 = obj.GetType().GetMethod("GetHostName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (method5 != null)
				{
					string text = ExtractSteamIdFromString(Convert.ToString(method5.Invoke(obj, null)));
					if (IsValidSteamId(text))
					{
						return text;
					}
				}
				string text2 = ExtractSteamIdFromString(obj.ToString());
				if (IsValidSteamId(text2))
				{
					return text2;
				}
			}
			string text3 = ExtractSteamIdFromString(znetPeer.ToString());
			if (IsValidSteamId(text3))
			{
				return text3;
			}
		}
		catch
		{
		}
		return "UNKNOWN";
	}

	private static bool TryNormalizeSteamId(object raw, out string normalized)
	{
		normalized = null;
		if (raw == null)
		{
			return false;
		}
		if (!(raw is ulong num))
		{
			if (!(raw is long num2))
			{
				if (raw is string text && IsValidSteamId(text))
				{
					normalized = text;
					return true;
				}
			}
			else if (num2 > 0)
			{
				normalized = num2.ToString();
				return true;
			}
		}
		else if (num != 0L)
		{
			normalized = num.ToString();
			return true;
		}
		Type type = raw.GetType();
		FieldInfo field = type.GetField("m_SteamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
		if (field != null)
		{
			object value = field.GetValue(raw);
			if (value != null && ulong.TryParse(value.ToString(), out var result) && result != 0L)
			{
				normalized = result.ToString();
				return true;
			}
		}
		PropertyInfo property = type.GetProperty("Value", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
		if (property != null)
		{
			object value2 = property.GetValue(raw, null);
			if (value2 != null && ulong.TryParse(value2.ToString(), out var result2) && result2 != 0L)
			{
				normalized = result2.ToString();
				return true;
			}
		}
		string text2 = ExtractSteamIdFromString(raw.ToString());
		if (IsValidSteamId(text2))
		{
			normalized = text2;
			return true;
		}
		return false;
	}

	private static string ExtractSteamIdFromString(string s)
	{
		if (string.IsNullOrEmpty(s))
		{
			return null;
		}
		int num = 0;
		int startIndex = -1;
		for (int i = 0; i < s.Length; i++)
		{
			if (char.IsDigit(s[i]))
			{
				if (num == 0)
				{
					startIndex = i;
				}
				num++;
				if (num == 17)
				{
					return s.Substring(startIndex, 17);
				}
			}
			else
			{
				num = 0;
				startIndex = -1;
			}
		}
		return null;
	}

	private static bool IsValidSteamId(string candidate)
	{
		if (string.IsNullOrWhiteSpace(candidate))
		{
			return false;
		}
		if (candidate.Length != 17)
		{
			return false;
		}
		for (int i = 0; i < candidate.Length; i++)
		{
			if (candidate[i] < '0' || candidate[i] > '9')
			{
				return false;
			}
		}
		return candidate != "00000000000000000";
	}

	private static string GetPeerPlayerName(object znetPeer)
	{
		return znetPeer.GetType().GetField("m_playerName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(znetPeer)?.ToString() ?? "Unknown";
	}

	private static string GetPeerCharacterId(object znetPeer)
	{
		return (znetPeer.GetType().GetField("m_characterID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(znetPeer))?.ToString() ?? "CHAR_UNKNOWN";
	}

	private bool IsAdmin(string platformId)
	{
		return _admins.Contains(platformId);
	}

	private string FormatPlayer(string steamId)
	{
		if (string.IsNullOrWhiteSpace(steamId))
		{
			return "NewPlayer (UNKNOWN)";
		}
		if (_registrations != null && _registrations.TryGetValue(steamId, out var value) && value != null && value.Count > 0)
		{
			return string.Join(", ", value) + " (" + steamId + ")";
		}
		return "NewPlayer (" + steamId + ")";
	}

	private void RecordMetricDetection(string modToken, string detectionMethod)
	{
		if (_settings.EnableMetrics && _metrics != null)
		{
			_metrics.total_mods_detected++;
			switch (detectionMethod)
			{
			case "RPC":
				_metrics.phase1_rpc_detections++;
				break;
			case "Assembly":
				_metrics.phase2_assembly_detections++;
				break;
			case "Version":
				_metrics.version_keyword_detections++;
				break;
			}
			if (!_metrics.top_detected_mods.ContainsKey(modToken))
			{
				_metrics.top_detected_mods[modToken] = 0L;
			}
			_metrics.top_detected_mods[modToken]++;
			SaveMetrics();
		}
	}

	private static string FriendlyReason(string rule, string detail = null)
	{
		switch (rule)
		{
		case "CompanionMissing":
			return "missing the required companion mod";
		case "HmacInvalid":
			return "wrong password";
		case "ChallengeMismatch":
			return "wrong password";
		case "RequiredModMissing":
			if (!string.IsNullOrEmpty(detail))
			{
				return "missing a required mod (" + detail + ")";
			}
			return "missing a required mod";
		case "DisallowedMod":
			if (!string.IsNullOrEmpty(detail))
			{
				return "had a mod that isn't allowed (" + detail + ")";
			}
			return "had a mod that isn't allowed";
		case "BannedMod":
			if (!string.IsNullOrEmpty(detail))
			{
				return "had a banned mod (" + detail + ")";
			}
			return "had a banned mod";
		case "HashMismatch":
			if (!string.IsNullOrEmpty(detail))
			{
				return "mod file doesn't match the server's copy (" + detail + ")";
			}
			return "mod file doesn't match the server's copy";
		case "CharacterNameLimitExceeded":
			return "tried to use too many characters";
		case "DevcommandAttempt":
			if (!string.IsNullOrEmpty(detail))
			{
				return "tried to use cheats (`" + detail + "`)";
			}
			return "tried to use cheats";
		case "SpeedHack":
			if (!string.IsNullOrEmpty(detail))
			{
				return "moved suspiciously fast (~" + detail + ")";
			}
			return "moved suspiciously fast";
		case "IllegalItem":
			if (!string.IsNullOrEmpty(detail))
			{
				return "had an unknown item (" + detail + ")";
			}
			return "had an unknown item";
		case "StackOverflow":
			if (!string.IsNullOrEmpty(detail))
			{
				return "had an over-sized item stack (" + detail + ")";
			}
			return "had an over-sized item stack";
		case "AnimationCancel":
			if (!string.IsNullOrEmpty(detail))
			{
				return "tried to cancel attack with " + detail;
			}
			return "tried to cancel attack animation";
		case "SkillOverflow":
			if (!string.IsNullOrEmpty(detail))
			{
				return "skill level above cap (" + detail + ")";
			}
			return "skill level above cap";
		default:
			return "policy violation";
		}
	}

	private bool RuleCountsAsViolation(string rule)
	{
		if (_settings?.CountAsViolation == null)
		{
			return false;
		}
		bool value;
		return _settings.CountAsViolation.TryGetValue(rule, out value) && value;
	}

	private void PostAdminEvent(string text)
	{
		if (_settings != null && !string.IsNullOrWhiteSpace(_settings.discordWebhookUrlAdmin) && !string.IsNullOrWhiteSpace(text))
		{
			SendDiscordNow(text, DiscordChannel.Admin);
		}
	}

	private void PostPlayerEvent(string emoji, string platformId, string action, string reason = null)
	{
		TrackEventForDailySummary(action, reason);
		string text = FormatPlayer(platformId);
		string text2 = (string.IsNullOrEmpty(reason) ? (emoji + " " + text + " " + action) : (emoji + " " + text + " " + action + " — " + reason));
		if (!string.IsNullOrWhiteSpace(platformId) && IsAdmin(platformId))
		{
			if (!string.IsNullOrWhiteSpace(_settings?.discordWebhookUrlAdmin))
			{
				SendDiscordNow(text2, DiscordChannel.Admin);
			}
		}
		else if (!string.IsNullOrWhiteSpace(_settings?.discordWebhookUrl))
		{
			SendDiscordNow(text2);
		}
	}

	private void TrackEventForDailySummary(string action, string reason)
	{
		if (string.IsNullOrEmpty(action))
		{
			return;
		}
		lock (_summaryLock)
		{
			if (action.IndexOf("joined", StringComparison.OrdinalIgnoreCase) >= 0)
			{
				_summaryJoins++;
			}
			else if (action.IndexOf("auto-banned", StringComparison.OrdinalIgnoreCase) >= 0)
			{
				_summaryBans++;
			}
			else if (action.IndexOf("kicked", StringComparison.OrdinalIgnoreCase) >= 0)
			{
				_summaryKicks++;
				string key = (string.IsNullOrWhiteSpace(reason) ? "other" : reason.Trim());
				_summaryKickReasons.TryGetValue(key, out var value);
				_summaryKickReasons[key] = value + 1;
			}
			else if (action.IndexOf("left", StringComparison.OrdinalIgnoreCase) >= 0)
			{
				_summaryLeaves++;
			}
		}
	}

	private List<SelfTestResult> RunSelfTest()
	{
		List<SelfTestResult> results = new List<SelfTestResult>();
		try
		{
			if (_settings.RequireHmac && string.IsNullOrEmpty(_settings.SharedSecret))
			{
				Add("HMAC sharedSecret", pass: false, "requireHmac=true but sharedSecret is empty");
			}
			else if (_settings.RequireHmac)
			{
				Add("HMAC sharedSecret", pass: true, $"len={_settings.SharedSecret.Length}");
			}
			else
			{
				Add("HMAC sharedSecret", pass: true, "requireHmac disabled - skipped");
			}
		}
		catch (Exception ex)
		{
			Add("HMAC sharedSecret", pass: false, ex.Message);
		}
		try
		{
			string canonical = new ModManifest
			{
				SchemaVersion = "1",
				Challenge = "selftest-challenge",
				TimestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
				Mods = new List<ModManifestEntry>()
			}.CanonicalForHmac();
			bool flag = ModManifest.ConstantTimeEquals(ModManifest.ComputeHmac(canonical, _settings.SharedSecret ?? ""), ModManifest.ComputeHmac(canonical, _settings.SharedSecret ?? ""));
			Add("HMAC roundtrip", flag, flag ? "sign/verify match" : "sign/verify MISMATCH");
		}
		catch (Exception ex2)
		{
			Add("HMAC roundtrip", pass: false, ex2.Message);
		}
		try
		{
			ModManifest manifest = new ModManifest
			{
				Mods = new List<ModManifestEntry>()
			};
			PolicyVerdict policyVerdict = ValidateAgainstPolicy(manifest);
			bool num = _requiredMods.Count > 0;
			bool pass2 = (num ? (!policyVerdict.Allowed) : policyVerdict.Allowed);
			string detail2 = ((!num) ? (policyVerdict.Allowed ? "empty manifest allowed (no required mods)" : "empty manifest unexpectedly rejected") : (policyVerdict.Allowed ? "empty manifest passed but required mods exist!" : ("empty manifest rejected as expected (" + policyVerdict.Rule + ")")));
			Add("Policy validator", pass2, detail2);
		}
		catch (Exception ex3)
		{
			Add("Policy validator", pass: false, ex3.Message);
		}
		try
		{
			Directory.CreateDirectory(BuildLogDir);
			string path = Path.Combine(BuildLogDir, ".selftest_probe");
			File.WriteAllText(path, "ok");
			File.Delete(path);
			Add("Build-log dir writable", pass: true, BuildLogDir);
		}
		catch (Exception ex4)
		{
			Add("Build-log dir writable", pass: false, ex4.Message);
		}
		try
		{
			bool flag2 = !string.IsNullOrEmpty(_modsetFingerprintLoose) && !string.IsNullOrEmpty(_modsetFingerprintStrict);
			Add("Modset fingerprint", flag2, flag2 ? ("loose=" + ModsetFingerprint.Short(_modsetFingerprintLoose) + " strict=" + ModsetFingerprint.Short(_modsetFingerprintStrict)) : "empty");
		}
		catch (Exception ex5)
		{
			Add("Modset fingerprint", pass: false, ex5.Message);
		}
		Add("Public webhook URL", string.IsNullOrEmpty(_settings.discordWebhookUrl) || IsWebhookUrlSane(_settings.discordWebhookUrl), string.IsNullOrEmpty(_settings.discordWebhookUrl) ? "not configured (ok)" : "looks valid");
		Add("Admin  webhook URL", string.IsNullOrEmpty(_settings.discordWebhookUrlAdmin) || IsWebhookUrlSane(_settings.discordWebhookUrlAdmin), string.IsNullOrEmpty(_settings.discordWebhookUrlAdmin) ? "not configured (ok)" : "looks valid");
		Add("Admins configured", _admins.Count > 0, (_admins.Count > 0) ? $"{_admins.Count} admin SteamID(s) in admins.yaml" : "admins.yaml is empty - sg commands will be unusable");
		return results;
		SelfTestResult Add(string name, bool pass, string detail)
		{
			SelfTestResult selfTestResult = new SelfTestResult
			{
				Name = name,
				Pass = pass,
				Detail = (detail ?? "")
			};
			results.Add(selfTestResult);
			return selfTestResult;
		}
	}

	private static bool IsWebhookUrlSane(string url)
	{
		if (string.IsNullOrWhiteSpace(url))
		{
			return false;
		}
		if (!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
		{
			return false;
		}
		if (url.IndexOf("/webhooks/", StringComparison.OrdinalIgnoreCase) < 0)
		{
			return false;
		}
		if (Uri.TryCreate(url, UriKind.Absolute, out Uri result))
		{
			return result.Host.IndexOf('.') >= 0;
		}
		return false;
	}

	private string FormatSelfTestReport(List<SelfTestResult> results)
	{
		int num = results.Count((SelfTestResult r) => r.Pass);
		int num2 = results.Count - num;
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.AppendLine($"[ServerGuard] Self-test  pass={num}  fail={num2}");
		foreach (SelfTestResult result in results)
		{
			string arg = (result.Pass ? "PASS" : "FAIL");
			stringBuilder.AppendLine($"  [{arg}]  {result.Name,-26}  {result.Detail}");
		}
		return stringBuilder.ToString().TrimEnd(Array.Empty<char>());
	}

	private string FormatSelfTestForDiscord(List<SelfTestResult> results)
	{
		int num = results.Count((SelfTestResult r) => r.Pass);
		int num2 = results.Count - num;
		StringBuilder stringBuilder = new StringBuilder();
		string arg = ((num2 == 0) ? ":white_check_mark:" : ":rotating_light:");
		stringBuilder.AppendLine($"{arg} **Self-test** — {num} pass / {num2} fail");
		if (num2 > 0)
		{
			foreach (SelfTestResult item in results.Where((SelfTestResult r) => !r.Pass))
			{
				stringBuilder.AppendLine("  • **" + item.Name + "**: " + item.Detail);
			}
		}
		return stringBuilder.ToString().TrimEnd(Array.Empty<char>());
	}

	[IteratorStateMachine(typeof(<PingLogLoop>d__112))]
	public IEnumerator PingLogLoop()
	{
		//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
		return new <PingLogLoop>d__112(0)
		{
			<>4__this = this
		};
	}

	private void TickPingLog()
	{
		List<ZNetPeer> peers = ZNet.instance.GetPeers();
		if (peers == null)
		{
			return;
		}
		foreach (ZNetPeer item in peers)
		{
			if (item == null || item.m_rpc == null)
			{
				continue;
			}
			string peerPlatformId = GetPeerPlatformId(item);
			if (string.IsNullOrWhiteSpace(peerPlatformId))
			{
				continue;
			}
			float num = ReadPingMs(item.m_rpc);
			if (!(num <= 0f) && !(num > 10000f))
			{
				if (!_pingState.TryGetValue(item.m_uid, out var value) || value == null)
				{
					value = new PingState();
					_pingState[item.m_uid] = value;
				}
				value.Samples.Add(num);
				if (value.Samples.Count > 200)
				{
					value.Samples.RemoveAt(0);
				}
				if (!value.FirstPosted && value.Samples.Count >= 2)
				{
					value.FirstPosted = true;
					PostAdminEvent($":satellite: **{FormatPlayer(peerPlatformId)}** first ping: **{num:F0} ms**");
				}
			}
		}
	}

	private static float ReadPingMs(object rpc)
	{
		if (rpc == null)
		{
			return 0f;
		}
		try
		{
			if (_rpcPingField == null)
			{
				_rpcPingField = rpc.GetType().GetField("m_ping", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			}
			if (_rpcPingField == null)
			{
				return 0f;
			}
			object value = _rpcPingField.GetValue(rpc);
			if (value is float num)
			{
				return num * 1000f;
			}
			if (value is double)
			{
				double num2 = (double)value;
				return (float)(num2 * 1000.0);
			}
		}
		catch
		{
		}
		return 0f;
	}

	internal void FlushPingOnDisconnect(long peerUid, string steamId)
	{
		if (_settings != null && _settings.EnablePingLog && _pingState.TryGetValue(peerUid, out var value) && value != null)
		{
			_pingState.Remove(peerUid);
			if (value.Samples.Count != 0)
			{
				float num = value.Samples.Sum() / (float)value.Samples.Count;
				PostAdminEvent($":satellite: **{FormatPlayer(steamId)}** session ping avg: **{num:F0} ms** ({value.Samples.Count} samples)");
			}
		}
	}

	[IteratorStateMachine(typeof(<SpeedCheckLoop>d__116))]
	public IEnumerator SpeedCheckLoop()
	{
		//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
		return new <SpeedCheckLoop>d__116(0)
		{
			<>4__this = this
		};
	}

	private void TickSpeedCheck()
	{
		//IL_0084: Unknown result type (might be due to invalid IL or missing references)
		//IL_0089: Unknown result type (might be due to invalid IL or missing references)
		//IL_0093: Unknown result type (might be due to invalid IL or missing references)
		//IL_0095: 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_00c3: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c8: Unknown result type (might be due to invalid IL or missing references)
		//IL_024d: Unknown result type (might be due to invalid IL or missing references)
		//IL_024f: Unknown result type (might be due to invalid IL or missing references)
		//IL_012a: Unknown result type (might be due to invalid IL or missing references)
		//IL_013e: Unknown result type (might be due to invalid IL or missing references)
		List<ZNetPeer> peers = ZNet.instance.GetPeers();
		if (peers == null)
		{
			return;
		}
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		float num = (float)_settings.SpeedCheckMaxMetersPerSecond;
		float num2 = (float)_settings.SpeedCheckTeleportToleranceMeters;
		int num3 = Math.Max(1, _settings.SpeedCheckConsecutiveStrikes);
		foreach (ZNetPeer item in peers)
		{
			if (item == null)
			{
				continue;
			}
			string peerPlatformId = GetPeerPlatformId(item);
			if (string.IsNullOrWhiteSpace(peerPlatformId) || IsAdmin(peerPlatformId))
			{
				continue;
			}
			ZDOID characterID;
			try
			{
				characterID = item.m_characterID;
			}
			catch
			{
				continue;
			}
			if (characterID == ZDOID.None)
			{
				continue;
			}
			ZDOMan instance = ZDOMan.instance;
			ZDO val = ((instance != null) ? instance.GetZDO(characterID) : null);
			if (val == null)
			{
				continue;
			}
			Vector3 position;
			try
			{
				position = val.GetPosition();
			}
			catch
			{
				continue;
			}
			if (!_speedState.TryGetValue(item.m_uid, out var value) || value == null)
			{
				value = new SpeedState();
				_speedState[item.m_uid] = value;
			}
			if (value.HasLastPos)
			{
				float num4 = realtimeSinceStartup - value.LastSampleTime;
				if (num4 > 0.01f)
				{
					float num5 = position.x - value.LastPos.x;
					float num6 = position.z - value.LastPos.z;
					float num7 = (float)Math.Sqrt(num5 * num5 + num6 * num6);
					float num8 = num7 / num4;
					if (num7 > num2)
					{
						value.OverThresholdCount = 0;
					}
					else if (num8 > num)
					{
						value.OverThresholdCount++;
						LogS.LogInfo((object)("[ServerGuard] Speed alert " + FormatPlayer(peerPlatformId) + ": " + $"{num8:F1} m/s over {num4:F2}s ({value.OverThresholdCount}/{num3})"));
						if (value.OverThresholdCount >= num3)
						{
							string text = $"{num8:F1} m/s";
							PostPlayerEvent(":runner:", peerPlatformId, "flagged for speed", "~" + text);
							AddViolation(peerPlatformId, "SpeedHack", text);
							value.OverThresholdCount = 0;
						}
					}
					else
					{
						value.OverThresholdCount = 0;
					}
				}
			}
			value.LastPos = position;
			value.LastSampleTime = realtimeSinceStartup;
			value.HasLastPos = true;
		}
	}

	internal List<string> ValidateInventoryItem(ItemData item)
	{
		List<string> list = new List<string>();
		if (item == null || item.m_shared == null)
		{
			return list;
		}
		string text = item.m_shared.m_name ?? "";
		string text2 = text.TrimStart(new char[1] { '$' });
		try
		{
			ObjectDB instance = ObjectDB.instance;
			if ((Object)(object)instance != (Object)null)
			{
				bool flag = false;
				if (instance.m_items != null)
				{
					foreach (GameObject item2 in instance.m_items)
					{
						if (!((Object)(object)item2 == (Object)null))
						{
							ItemDrop component = item2.GetComponent<ItemDrop>();
							if (!((Object)(object)component == (Object)null) && component.m_itemData?.m_shared != null && string.Equals(component.m_itemData.m_shared.m_name, text, StringComparison.Ordinal))
							{
								flag = true;
								break;
							}
						}
					}
				}
				if (!flag)
				{
					list.Add("unknown item '" + text2 + "'");
				}
			}
		}
		catch
		{
		}
		try
		{
			int num = Math.Max(1, item.m_shared.m_maxStackSize);
			double num2 = Math.Max(1.0, _settings.InventoryCheckStackTolerance);
			int num3 = (int)Math.Ceiling((double)num * num2);
			if (item.m_stack > num3)
			{
				list.Add($"stack {item.m_stack} > max {num} for '{text2}'");
			}
		}
		catch
		{
		}
		return list;
	}

	[IteratorStateMachine(typeof(<DailySummaryLoop>d__120))]
	private IEnumerator DailySummaryLoop()
	{
		//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
		return new <DailySummaryLoop>d__120(0)
		{
			<>4__this = this
		};
	}

	private double SecondsUntilNextFire()
	{
		int hour = Math.Max(0, Math.Min(23, _settings.DailySummaryHourUtc));
		DateTime utcNow = DateTime.UtcNow;
		DateTime dateTime = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, hour, 0, 0, DateTimeKind.Utc);
		DateTime dateTime2 = ((utcNow < dateTime) ? dateTime : dateTime.AddDays(1.0));
		return Math.Max(60.0, (dateTime2 - utcNow).TotalSeconds);
	}

	private async Task PostDailySummary(bool scheduled)
	{
		int summaryJoins;
		int summaryLeaves;
		int summaryKicks;
		int summaryBans;
		DateTime summarySince;
		List<KeyValuePair<string, int>> list;
		lock (_summaryLock)
		{
			summaryJoins = _summaryJoins;
			summaryLeaves = _summaryLeaves;
			summaryKicks = _summaryKicks;
			summaryBans = _summaryBans;
			summarySince = _summarySince;
			list = _summaryKickReasons.OrderByDescending((KeyValuePair<string, int> kv) => kv.Value).Take(5).ToList();
			_summaryJoins = 0;
			_summaryLeaves = 0;
			_summaryKicks = 0;
			_summaryBans = 0;
			_summaryKickReasons.Clear();
			_summarySince = DateTime.UtcNow;
		}
		if (summaryJoins == 0 && summaryLeaves == 0 && summaryKicks == 0 && summaryBans == 0 && scheduled)
		{
			return;
		}
		string value = $"{summarySince:yyyy-MM-dd HH:mm} UTC → {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC";
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.AppendLine(":bar_chart: **Daily summary**");
		stringBuilder.AppendLine(value);
		stringBuilder.AppendLine($"• Joins: **{summaryJoins}**");
		stringBuilder.AppendLine($"• Leaves: **{summaryLeaves}**");
		stringBuilder.AppendLine($"• Kicks: **{summaryKicks}**");
		stringBuilder.AppendLine($"• Auto-bans: **{summaryBans}**");
		if (list.Count > 0)
		{
			stringBuilder.AppendLine("Top kick reasons:");
			foreach (KeyValuePair<string, int> item in list)
			{
				stringBuilder.AppendLine($"  – {item.Key} ({item.Value})");
			}
		}
		string text = (_settings.DailySummaryChannel ?? "admin").ToLowerInvariant();
		DiscordChannel discordChannel = ((!(text == "public")) ? ((!(text == "both")) ? DiscordChannel.Admin : DiscordChannel.Both) : DiscordChannel.Public);
		DiscordChannel target = discordChannel;
		await SendDiscordNow(stringBuilder.ToString(), target);
	}

	private void AddViolation(string platformId, string rule, string detail = null)
	{
		bool num = RuleCountsAsViolation(rule);
		string text = FormatPlayer(platformId);
		if (num)
		{
			if (!_violations.TryGetValue(platformId, out var value))
			{
				value = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
				_violations[platformId] = value;
			}
			value.TryGetValue(rule, out var value2);
			value[rule] = value2 + 1;
			SaveViolations();
			if (_settings.EnableMetrics)
			{
				_metrics.violations_issued++;
				SaveMetrics();
			}
			LogS.LogWarning((object)$"[ServerGuard] {text} violated {rule}. Count={value[rule]}/{_settings.ViolationThreshold}");
			PostAdminEvent($":warning: **{text}** violated **{rule}** ({value[rule]}/{_settings.ViolationThreshold})" + (string.IsNullOrEmpty(detail) ? "" : (" — " + detail)));
			if (_settings.Enforce && value[rule] >= _settings.ViolationThreshold)
			{
				TryBan(platformId, _settings.BanReason);
				if (_settings.EnableMetrics)
				{
					_metrics.players_banned++;
					SaveMetrics();
				}
				PostPlayerEvent(":no_entry:", platformId, "was auto-banned", "too many strikes");
				PostAdminEvent(":no_entry: Auto-banned **" + text + "** (threshold reached)");
			}
		}
		else
		{
			LogS.LogWarning((object)("[ServerGuard] " + text + " hit rule '" + rule + "' (countAsViolation: false - no strike recorded)."));
			PostAdminEvent(":eye: **" + text + "** triggered **" + rule + "** (informational — not counted)" + (string.IsNullOrEmpty(detail) ? "" : (" — " + detail)));
		}
	}

	private void TryKick(object znetPeer, string reason)
	{
		try
		{
			ZNetPeer val = (ZNetPeer)((znetPeer is ZNetPeer) ? znetPeer : null);
			if (val == null || val == null || (Object)(object)ZNet.instance == (Object)null)
			{
				return;
			}
			string peerPlatformId = GetPeerPlatformId(val);
			string text = FormatPlayer(peerPlatformId);
			try
			{
				ZRpc rpc = val.m_rpc;
				if (rpc != null)
				{
					rpc.Invoke("Error", new object[1] { 3 });
				}
			}
			catch
			{
			}
			try
			{
				lock (_suppressLogoutFor)
				{
					_suppressLogoutFor.Add(val.m_uid);
				}
			}
			catch
			{
			}
			try
			{
				ZNet.instance.Disconnect(val);
				LogS.LogWarning((object)("[ServerGuard] Disconnected " + text + ". Reason: " + reason));
				PostAdminEvent(":door: Disconnected **" + text + "** — " + reason);
				return;
			}
			catch (Exception ex)
			{
				LogS.LogWarning((object)("[ServerGuard] ZNet.Disconnect threw (" + ex.Message + "); falling back to reflection."));
			}
			object obj3 = typeof(ZNet).GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null);
			if (obj3 == null)
			{
				return;
			}
			MethodInfo method = obj3.GetType().GetMethod("Disconnect", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(ZNetPeer) }, null);
			if (method != null)
			{
				method.Invoke(obj3, new object[1] { val });
				LogS.LogWarning((object)("[ServerGuard] Disconnected " + text + " (reflection). Reason: " + reason));
				return;
			}
			MethodInfo method2 = obj3.GetType().GetMethod("InternalKick", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(ZNetPeer) }, null);
			if (method2 != null)
			{
				method2.Invoke(obj3, new object[1] { val });
				LogS.LogWarning((object)("[ServerGuard] InternalKick'd " + text + ". Reason: " + reason));
			}
		}
		catch (Exception arg)
		{
			LogS.LogError((object)$"[ServerGuard] Kick failed: {arg}");
		}
	}

	private void TryBan(string platformId, string reason)
	{
		try
		{
			object obj = typeof(ZNet).GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null);
			if (obj != null)
			{
				MethodInfo method = obj.GetType().GetMethod("Ban", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null);
				if (method != null)
				{
					method.Invoke(obj, new object[1] { platformId });
					string text = FormatPlayer(platformId);
					LogS.LogWarning((object)("[ServerGuard] Auto-banned " + text + ". Reason: " + reason));
				}
			}
		}
		catch (Exception arg)
		{
			LogS.LogError((object)$"[ServerGuard] Ban failed: {arg}");
		}
	}

	private static ZNetPeer ResolvePeerFromRpc(ZNet znet, ZRpc rpc)
	{
		//IL_0054: Unknown result type (might be due to invalid IL or missing references)
		//IL_005a: Expected O, but got Unknown
		//IL_00dc: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e2: Expected O, but got Unknown
		if ((Object)(object)znet == (Object)null || rpc == null)
		{
			return null;
		}
		MethodInfo method = typeof(ZNet).GetMethod("GetPeer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(ZRpc) }, null);
		if (method != null)
		{
			return (ZNetPeer)method.Invoke(znet, new object[1] { rpc });
		}
		MethodInfo method2 = ((object)rpc).GetType().GetMethod("GetUID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
		if (method2 != null && method2.Invoke(rpc, null) is long num)
		{
			MethodInfo method3 = typeof(ZNet).GetMethod("GetPeer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(long) }, null);
			if (method3 != null)
			{
				return (ZNetPeer)method3.Invoke(znet, new object[1] { num });
			}
		}
		LogS.LogWarning((object)"[ServerGuard] ResolvePeerFromRpc: unable to resolve peer from ZRpc.");
		return null;
	}

	private string GenerateChallenge()
	{
		byte[] array = new byte[24];
		using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create())
		{
			randomNumberGenerator.GetBytes(array);
		}
		return Convert.ToBase64String(array);
	}

	private static string GenerateSharedSecret()