Decompiled source of ValheimDeathBan v1.1.13

ValheimDeathBan.dll

Decompiled 2 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("ValheimDeathBan")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ValheimDeathBan")]
[assembly: AssemblyCopyright("Copyright ©  2026")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("F05960A1-C2AA-4AEB-BA68-6DC9426B2BC0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace ValheimDeathBan;

internal sealed class DeathAnnouncementUi : IDisposable
{
	private readonly Queue<string> _messages = new Queue<string>();

	private readonly ManualLogSource _log;

	private Component _label;

	private float _until;

	private float _retryAt;

	private bool _fontWarning;

	internal DeathAnnouncementUi(ManualLogSource log)
	{
		_log = log;
	}

	internal void Enqueue(string text)
	{
		if (_messages.Count >= 100)
		{
			_messages.Dequeue();
		}
		_messages.Enqueue(text);
	}

	internal void Tick(float seconds)
	{
		if ((Object)(object)_label != (Object)null && Time.realtimeSinceStartup < _until)
		{
			return;
		}
		if ((Object)(object)_label != (Object)null)
		{
			_label.gameObject.SetActive(false);
		}
		if (_messages.Count == 0 || Time.realtimeSinceStartup < _retryAt)
		{
			return;
		}
		try
		{
			if ((Object)(object)_label == (Object)null && !CreateLabel())
			{
				_retryAt = Time.realtimeSinceStartup + 1f;
				return;
			}
			Set("text", _messages.Peek());
			_messages.Dequeue();
			_label.gameObject.SetActive(true);
			_label.transform.SetAsLastSibling();
			_until = Time.realtimeSinceStartup + seconds;
		}
		catch (Exception ex)
		{
			_retryAt = Time.realtimeSinceStartup + 10f;
			_log.LogWarning((object)("Death banner UI failed: " + ex.GetType().Name));
		}
	}

	private bool CreateLabel()
	{
		//IL_00bf: 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_00e9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00fe: Unknown result type (might be due to invalid IL or missing references)
		//IL_0113: Unknown result type (might be due to invalid IL or missing references)
		//IL_011d: Unknown result type (might be due to invalid IL or missing references)
		//IL_01b3: Unknown result type (might be due to invalid IL or missing references)
		//IL_01dd: Unknown result type (might be due to invalid IL or missing references)
		if ((Object)(object)MessageHud.instance == (Object)null)
		{
			return false;
		}
		object? obj = typeof(MessageHud).GetField("m_messageCenterText", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(MessageHud.instance);
		Component val = (Component)((obj is Component) ? obj : null);
		if ((Object)(object)val == (Object)null)
		{
			return false;
		}
		Transform parent = val.transform.parent;
		while ((Object)(object)parent != (Object)null && (Object)(object)((Component)parent).GetComponent("Canvas") == (Object)null)
		{
			parent = parent.parent;
		}
		if ((Object)(object)parent == (Object)null)
		{
			return false;
		}
		GameObject val2 = Object.Instantiate<GameObject>(val.gameObject, parent, false);
		((Object)val2).name = "ValheimDeathBanAnnouncement";
		_label = val2.GetComponent(((object)val).GetType());
		RectTransform component = val2.GetComponent<RectTransform>();
		component.anchorMin = new Vector2(0.05f, 1f);
		component.anchorMax = new Vector2(0.95f, 1f);
		component.pivot = new Vector2(0.5f, 1f);
		component.sizeDelta = new Vector2(0f, 180f);
		component.anchoredPosition = new Vector2(0f, -60f);
		((Transform)component).localScale = Vector3.one;
		Set("richText", false);
		Set("raycastTarget", false);
		Set("enableAutoSizing", true);
		Set("fontSizeMin", 30f);
		Set("fontSizeMax", 60f);
		Set("fontSize", 60f);
		Set("color", (object)new Color(1f, 0.12f, 0.12f, 1f));
		Set("alpha", 1f);
		Set("outlineColor", Color.black);
		Set("outlineWidth", 0.18f);
		PropertyInfo property = ((object)_label).GetType().GetProperty("alignment");
		if (property != null)
		{
			property.SetValue(_label, Enum.Parse(property.PropertyType, "Center"), null);
		}
		PropertyInfo property2 = ((object)_label).GetType().GetProperty("font");
		Object val3 = null;
		if (property2 != null)
		{
			Object[] array = Resources.FindObjectsOfTypeAll(property2.PropertyType);
			foreach (Object val4 in array)
			{
				if (val4.name.Equals("Valheim-Norsebold", StringComparison.OrdinalIgnoreCase))
				{
					val3 = val4;
					break;
				}
			}
		}
		if (val3 != (Object)null)
		{
			property2.SetValue(_label, val3, null);
		}
		PropertyInfo property3 = ((object)_label).GetType().GetProperty("fontStyle");
		if (property3 != null)
		{
			property3.SetValue(_label, Enum.Parse(property3.PropertyType, "Bold"), null);
		}
		if (val3 == (Object)null && !_fontWarning)
		{
			_fontWarning = true;
			_log.LogWarning((object)"Norse font asset not found; death banner uses the game's message font.");
		}
		val2.SetActive(false);
		return true;
	}

	private void Set(string property, object value)
	{
		PropertyInfo property2 = ((object)_label).GetType().GetProperty(property);
		if (property2 != null && property2.CanWrite)
		{
			property2.SetValue(_label, value, null);
		}
	}

	public void Dispose()
	{
		_messages.Clear();
		if ((Object)(object)_label != (Object)null)
		{
			Object.Destroy((Object)(object)_label.gameObject);
		}
		_label = null;
	}
}
internal static class DeathMessageFormatter
{
	private static readonly Dictionary<string, string> Names = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
	{
		{ "enemy_gdking", "Древний" },
		{ "The Elder", "Древний" },
		{ "enemy_eikthyr", "Эйктюр" },
		{ "Eikthyr", "Эйктюр" },
		{ "enemy_bonemass", "Масса костей" },
		{ "Bonemass", "Масса костей" },
		{ "enemy_dragon", "Моудер" },
		{ "Moder", "Моудер" },
		{ "enemy_goblinking", "Яглут" },
		{ "Yagluth", "Яглут" },
		{ "enemy_seekerqueen", "Королева" },
		{ "The Queen", "Королева" },
		{ "enemy_fader", "Фейдер" },
		{ "Fader", "Фейдер" },
		{ "enemy_troll", "Тролль" },
		{ "Troll", "Тролль" },
		{ "enemy_greydwarf", "Грейдворф" },
		{ "Greydwarf", "Грейдворф" },
		{ "enemy_greydwarfbrute", "Грейдворф-громила" },
		{ "enemy_greydwarfshaman", "Грейдворф-шаман" },
		{ "enemy_greyling", "Грейлинг" },
		{ "enemy_boar", "Кабан" },
		{ "enemy_neck", "Никс" },
		{ "enemy_deer", "Олень" },
		{ "enemy_skeleton", "Скелет" },
		{ "enemy_draugr", "Драугр" },
		{ "enemy_draugrelite", "Элитный драугр" },
		{ "enemy_wraith", "Призрак" },
		{ "enemy_leech", "Пиявка" },
		{ "enemy_blob", "Слизень" },
		{ "enemy_wolf", "Волк" },
		{ "enemy_fenring", "Фенринг" },
		{ "enemy_stonegolem", "Каменный голем" },
		{ "enemy_hatchling", "Дрейк" },
		{ "enemy_goblin", "Фулинг" },
		{ "enemy_goblinbrute", "Фулинг-берсерк" },
		{ "enemy_goblinshaman", "Фулинг-шаман" },
		{ "enemy_deathsquito", "Комар смерти" },
		{ "enemy_lox", "Быкозавр" },
		{ "enemy_serpent", "Морской змей" },
		{ "enemy_surtling", "Суртлинг" },
		{ "enemy_abomination", "Мерзость" },
		{ "enemy_seeker", "Искатель" },
		{ "enemy_seekerbrute", "Солдат-искатель" },
		{ "enemy_gjall", "Гьялл" },
		{ "enemy_tick", "Клещ" },
		{ "enemy_dvergr", "Двергр" },
		{ "enemy_jotun_warrior", "Криген" }
	};

	private static bool _loaded;

	internal static string Format(string player, string reason)
	{
		reason = reason ?? "";
		int num = reason.IndexOf('[');
		int num2 = reason.IndexOf(']');
		string text = ((num >= 0 && num2 > num) ? reason.Substring(num + 1, num2 - num - 1) : "");
		if (text == "EnemyHit")
		{
			int num3 = reason.IndexOf("; attacker=", StringComparison.Ordinal);
			return Safe(((num3 >= 0) ? RussianName(reason.Substring(num3 + 11).Trim()) : "Неизвестное существо") + " убивает " + player);
		}
		switch (text)
		{
		case "Burning":
		case "CinderFire":
			return Safe(player + " сгорел");
		case "Freezing":
			return Safe(player + " замёрз");
		case "Poisoned":
			return Safe(player + " отравился");
		case "Fall":
			return Safe(player + " разбился");
		case "Tree":
			return Safe("Дерево придавило " + player);
		case "Drowning":
			return Safe(player + " утонул");
		default:
			return Safe(player + " усоп");
		}
	}

	private static string RussianName(string value)
	{
		LoadRussianTables();
		if (Names.TryGetValue(value.TrimStart(new char[1] { '$' }), out var value2))
		{
			return value2;
		}
		foreach (char c in value)
		{
			if (c >= 'Ѐ' && c <= 'ӿ')
			{
				return value;
			}
		}
		return "Неизвестное существо";
	}

	private static void LoadRussianTables()
	{
		if (_loaded)
		{
			return;
		}
		_loaded = true;
		try
		{
			TextAsset[] array = Resources.FindObjectsOfTypeAll<TextAsset>();
			foreach (TextAsset val in array)
			{
				if ((Object)(object)val == (Object)null || val.text.IndexOf("Russian", StringComparison.OrdinalIgnoreCase) < 0)
				{
					continue;
				}
				List<List<string>> list = ParseCsv(val.text);
				if (list.Count == 0)
				{
					continue;
				}
				int num = -1;
				int num2 = -1;
				for (int j = 0; j < list[0].Count; j++)
				{
					if (list[0][j].Trim().Equals("Russian", StringComparison.OrdinalIgnoreCase))
					{
						num = j;
					}
					if (list[0][j].Trim().Equals("English", StringComparison.OrdinalIgnoreCase))
					{
						num2 = j;
					}
				}
				if (num < 0)
				{
					continue;
				}
				for (int k = 1; k < list.Count; k++)
				{
					List<string> list2 = list[k];
					if (list2.Count <= num || string.IsNullOrWhiteSpace(list2[num]))
					{
						continue;
					}
					string text = list2[0].Trim().TrimStart(new char[1] { '$' });
					if (text.StartsWith("enemy_", StringComparison.OrdinalIgnoreCase))
					{
						Names[text] = list2[num].Trim();
						if (num2 >= 0 && list2.Count > num2 && !string.IsNullOrWhiteSpace(list2[num2]))
						{
							Names[list2[num2].Trim()] = list2[num].Trim();
						}
					}
				}
			}
		}
		catch (Exception)
		{
		}
	}

	internal static List<List<string>> ParseCsv(string csv)
	{
		List<List<string>> list = new List<List<string>>();
		List<string> list2 = new List<string>();
		StringBuilder stringBuilder = new StringBuilder();
		bool flag = false;
		for (int i = 0; i < csv.Length; i++)
		{
			char c = csv[i];
			if (c == '"')
			{
				if (flag && i + 1 < csv.Length && csv[i + 1] == '"')
				{
					stringBuilder.Append('"');
					i++;
				}
				else
				{
					flag = !flag;
				}
			}
			else if (!flag && (c == ',' || c == '\n' || c == '\r'))
			{
				list2.Add(stringBuilder.ToString());
				stringBuilder.Clear();
				if (c != ',')
				{
					list.Add(list2);
					list2 = new List<string>();
					if (c == '\r' && i + 1 < csv.Length && csv[i + 1] == '\n')
					{
						i++;
					}
				}
			}
			else
			{
				stringBuilder.Append(c);
			}
		}
		if (stringBuilder.Length > 0 || list2.Count > 0)
		{
			list2.Add(stringBuilder.ToString());
			list.Add(list2);
		}
		return list;
	}

	private static string Safe(string text)
	{
		return text.Replace('\r', ' ').Replace('\n', ' ').Replace('<', '<')
			.Replace('>', '>');
	}
}
internal sealed class DiscordDeathNotifier : IDisposable
{
	private sealed class Message
	{
		internal Uri Url;

		internal string Json;
	}

	private readonly Queue<Message> _queue = new Queue<Message>();

	private readonly object _gate = new object();

	private readonly CancellationTokenSource _stop = new CancellationTokenSource();

	private readonly HttpClient _http;

	private readonly ManualLogSource _log;

	private bool _running;

	private bool _disposed;

	internal DiscordDeathNotifier(ManualLogSource log)
	{
		//IL_002f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0034: Unknown result type (might be due to invalid IL or missing references)
		//IL_0040: Expected O, but got Unknown
		//IL_003b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0045: Expected O, but got Unknown
		_log = log;
		_http = new HttpClient((HttpMessageHandler)new HttpClientHandler
		{
			AllowAutoRedirect = false
		});
		_http.Timeout = TimeSpan.FromSeconds(10.0);
	}

	internal void Enqueue(string webhook, string player, string id, string reason, string status, DateTimeOffset time)
	{
		if (string.IsNullOrWhiteSpace(webhook))
		{
			return;
		}
		if (!Uri.TryCreate(webhook.Trim(), UriKind.Absolute, out Uri result) || result.Scheme != "https" || !result.IsDefaultPort || (!(result.Host == "discord.com") && !(result.Host == "discordapp.com") && !(result.Host == "canary.discord.com") && !(result.Host == "ptb.discord.com")) || !Regex.IsMatch(result.AbsolutePath, "^/api/(?:v\\d+/)?webhooks/\\d+/[A-Za-z0-9_-]+/?$") || !string.IsNullOrEmpty(result.UserInfo))
		{
			_log.LogWarning((object)"Discord: invalid webhook URL. Use the HTTPS URL copied from Discord.");
			return;
		}
		UriBuilder uriBuilder = new UriBuilder(result);
		string input = uriBuilder.Query.TrimStart(new char[1] { '?' });
		input = Regex.Replace(input, "(^|&)wait=[^&]*", "").Trim(new char[1] { '&' });
		uriBuilder.Query = ((input.Length == 0) ? "" : (input + "&")) + "wait=true";
		string value = DeathMessageFormatter.Format(Limit(player, 256), Limit(reason, 512)) + "\nID: " + Limit(id, 128) + "\nВремя: " + time.ToString("yyyy-MM-dd HH:mm:ss zzz", CultureInfo.InvariantCulture) + "\nСтатус: " + Limit(status, 256);
		Message item = new Message
		{
			Url = uriBuilder.Uri,
			Json = "{\"content\":" + Quote(value) + ",\"allowed_mentions\":{\"parse\":[]}}"
		};
		lock (_gate)
		{
			if (_disposed)
			{
				return;
			}
			if (_queue.Count >= 256)
			{
				_log.LogWarning((object)"Discord: notification queue full; this notification was dropped.");
				return;
			}
			_queue.Enqueue(item);
			if (!_running)
			{
				_running = true;
				Task.Run((Func<Task?>)DrainAsync);
			}
		}
	}

	private async Task DrainAsync()
	{
		_ = 1;
		try
		{
			while (true)
			{
				Message message;
				lock (_gate)
				{
					if (_disposed || _queue.Count == 0)
					{
						break;
					}
					message = _queue.Dequeue();
				}
				await SendAsync(message).ConfigureAwait(continueOnCapturedContext: false);
				await Task.Delay(500, _stop.Token).ConfigureAwait(continueOnCapturedContext: false);
			}
		}
		catch (OperationCanceledException)
		{
		}
		catch (Exception)
		{
			_log.LogWarning((object)"Discord: notification worker stopped unexpectedly.");
		}
		finally
		{
			lock (_gate)
			{
				_running = false;
				if (_disposed)
				{
					((HttpMessageInvoker)_http).Dispose();
				}
				else if (_queue.Count > 0)
				{
					_running = true;
					Task.Run((Func<Task?>)DrainAsync);
				}
			}
		}
	}

	private async Task SendAsync(Message message)
	{
		_ = 2;
		try
		{
			for (int attempt = 0; attempt < 3; attempt++)
			{
				StringContent body = new StringContent(message.Json, Encoding.UTF8, "application/json");
				try
				{
					HttpResponseMessage response = await _http.PostAsync(message.Url, (HttpContent)(object)body, _stop.Token).ConfigureAwait(continueOnCapturedContext: false);
					try
					{
						if (response.IsSuccessStatusCode)
						{
							break;
						}
						if (response.StatusCode == HttpStatusCode.TooManyRequests && attempt < 2)
						{
							Match match = Regex.Match(await response.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false), "\"retry_after\"\\s*:\\s*([0-9]+(?:\\.[0-9]+)?)");
							if (!match.Success || !double.TryParse(match.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
							{
								result = 2.0;
							}
							if (result > 60.0)
							{
								_log.LogWarning((object)"Discord: long rate limit; notification dropped.");
								break;
							}
							await Task.Delay(TimeSpan.FromSeconds(Math.Max(0.5, result) + 0.25), _stop.Token).ConfigureAwait(continueOnCapturedContext: false);
							continue;
						}
						_log.LogWarning((object)("Discord: HTTP " + (int)response.StatusCode + "; notification not delivered."));
						break;
					}
					finally
					{
						((IDisposable)response)?.Dispose();
					}
				}
				finally
				{
					((IDisposable)body)?.Dispose();
				}
			}
		}
		catch (OperationCanceledException)
		{
			if (!_stop.IsCancellationRequested)
			{
				_log.LogWarning((object)"Discord: request timed out; notification not delivered.");
			}
		}
		catch (Exception)
		{
			if (!_stop.IsCancellationRequested)
			{
				_log.LogWarning((object)"Discord: network/TLS error; notification not delivered.");
			}
		}
	}

	private static string Limit(string value, int length)
	{
		value = (value ?? "unknown").Replace('\r', ' ').Replace('\n', ' ');
		if (value.Length <= length)
		{
			return value;
		}
		return value.Substring(0, length);
	}

	internal static string Quote(string value)
	{
		StringBuilder stringBuilder = new StringBuilder("\"");
		foreach (char c in value)
		{
			if (c == '"' || c == '\\')
			{
				stringBuilder.Append('\\').Append(c);
			}
			else if (c < ' ' || char.IsSurrogate(c))
			{
				StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
				int num = c;
				stringBuilder2.Append(num.ToString("x4"));
			}
			else
			{
				stringBuilder.Append(c);
			}
		}
		return stringBuilder.Append('"').ToString();
	}

	public void Dispose()
	{
		lock (_gate)
		{
			if (!_disposed)
			{
				_disposed = true;
				_queue.Clear();
				_stop.Cancel();
				if (!_running)
				{
					((HttpMessageInvoker)_http).Dispose();
				}
			}
		}
	}
}
[BepInPlugin("com.xtasy.ValheimDeathBan", "Valheim Death Ban", "1.1.13")]
public sealed class ValheimDeathBanPlugin : BaseUnityPlugin
{
	private sealed class PendingMachine
	{
		internal string PlayerName;

		internal string Host;

		internal float Since;
	}

	private sealed class CharacterWatch
	{
		internal long Sender;

		internal string PlayerName;

		internal string Host;

		internal float LastSeen;

		internal bool Accepted;
	}

	private sealed class ReasonReport
	{
		internal string Host;

		internal string Reason;

		internal float ReceivedAt;
	}

	public const string GUID = "com.xtasy.ValheimDeathBan";

	public const string NAME = "Valheim Death Ban";

	public const string VERSION = "1.1.13";

	internal static ManualLogSource Log;

	internal static ValheimDeathBanPlugin Instance;

	internal static ConfigEntry<bool> Enabled;

	internal static ConfigEntry<float> KickDelaySeconds;

	internal static ConfigEntry<bool> BanAdmins;

	private ConfigEntry<float> _scanIntervalSeconds;

	private Harmony _harmony;

	private ConfigEntry<bool> _discordEnabled;

	private ConfigEntry<string> _webhookUrl;

	private DiscordDeathNotifier _discord;

	private ConfigEntry<bool> _announceChat;

	private ConfigEntry<bool> _announceBanner;

	private ConfigEntry<float> _bannerSeconds;

	private DeathAnnouncementUi _announcementUi;

	private const string AnnouncementRpc = "xtasy_DeathBan_Announcement_v1";

	private const string MachineRpc = "xtasy_DeathBan_Machine_v1";

	private ConfigEntry<bool> _machineCheckEnabled;

	private ConfigEntry<string> _blockedMachines;

	private ConfigEntry<string> _machineMatchText;

	private ConfigEntry<string> _machineNoMatchText;

	private readonly Dictionary<long, string> _machineSeen = new Dictionary<long, string>();

	private readonly Dictionary<long, PendingMachine> _machinePending = new Dictionary<long, PendingMachine>();

	private ZRoutedRpc _machineSentRouter;

	private ZRoutedRpc _registeredRouter;

	private const string ReasonRpc = "xtasy_DeathBan_Reason_v1";

	private readonly Dictionary<long, ReasonReport> _reports = new Dictionary<long, ReasonReport>();

	private readonly Dictionary<ZDOID, CharacterWatch> _characters = new Dictionary<ZDOID, CharacterWatch>();

	private float _nextScan;

	private float _nextErrorLog;

	private ConfigEntry<bool> _diagnosticsEnabled;

	private ConfigEntry<float> _diagnosticsIntervalMinutes;

	private float _nextDiagnostics;

	private long _diagnosticScans;

	private long _diagnosticFailures;

	private long _scanTotalTicks;

	private long _scanMaxTicks;

	private long _scanLastTicks;

	private long _peerTotalTicks;

	private long _peerMaxTicks;

	private long _peerLastTicks;

	private long _lookupTotalTicks;

	private long _lookupMaxTicks;

	private long _lookupLastTicks;

	private long _totalPeers;

	private long _totalZdoLookups;

	private long _totalDeadReads;

	private int _lastPeers;

	private int _lastZdoLookups;

	private int _lastDeadReads;

	private int _lastMissingZdos;

	private static readonly string MachineLabel = GetMachineLabel();

	private void AnnounceDeath(string playerName, string reason)
	{
		if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || ZRoutedRpc.instance == null)
		{
			return;
		}
		string text = DeathMessageFormatter.Format(Clean(playerName), Clean(reason));
		try
		{
			if (_announceChat.Value || _announceBanner.Value)
			{
				ZRoutedRpc.instance.InvokeRoutedRPC(0L, "xtasy_DeathBan_Announcement_v1", new object[3] { text, _announceChat.Value, _announceBanner.Value });
			}
		}
		catch (Exception ex)
		{
			Log.LogWarning((object)("Death announcement failed: " + ex.GetType().Name));
		}
	}

	private void ReceiveAnnouncement(long sender, string text, bool chat, bool banner)
	{
		if ((Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null || (!ZNet.instance.IsServer() && sender != DeathBanUtil.GetServerPeerId(ZRoutedRpc.instance)))
		{
			return;
		}
		text = Clean(text).Replace('<', '<').Replace('>', '>');
		if (chat && (Object)(object)Chat.instance != (Object)null)
		{
			try
			{
				MethodInfo? method = typeof(Terminal).GetMethod("AddString", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[4]
				{
					typeof(string),
					typeof(string),
					typeof(Type),
					typeof(bool)
				}, null);
				if (method == null)
				{
					throw new MissingMethodException("Terminal.AddString");
				}
				method.Invoke(Chat.instance, new object[4]
				{
					"Death Ban",
					text,
					(object)(Type)2,
					false
				});
				typeof(Chat).GetField("m_hideTimer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.SetValue(Chat.instance, 0f);
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("Death chat display failed: " + ex.GetType().Name));
			}
		}
		if (banner)
		{
			if (_announcementUi == null)
			{
				_announcementUi = new DeathAnnouncementUi(Log);
			}
			_announcementUi.Enqueue(text);
		}
	}

	private void NotifyDeath(string playerName, string host, string reason, string status, DateTimeOffset diedAt)
	{
		try
		{
			if (_discordEnabled.Value && _discord != null && (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())
			{
				_discord.Enqueue(_webhookUrl.Value, playerName, host, reason, status, diedAt);
			}
		}
		catch (Exception)
		{
			Log.LogWarning((object)"Discord notification could not be queued. Ban and journal are unaffected.");
		}
	}

	private void ResetDiagnostics(float now)
	{
		_nextDiagnostics = now + _diagnosticsIntervalMinutes.Value * 60f;
		_diagnosticScans = (_diagnosticFailures = 0L);
		_scanTotalTicks = (_scanMaxTicks = (_scanLastTicks = 0L));
		_peerTotalTicks = (_peerMaxTicks = (_peerLastTicks = 0L));
		_lookupTotalTicks = (_lookupMaxTicks = (_lookupLastTicks = 0L));
		_totalPeers = (_totalZdoLookups = (_totalDeadReads = 0L));
	}

	private static double Milliseconds(long ticks)
	{
		return (double)ticks * (1000.0 / (double)Stopwatch.Frequency);
	}

	private void RecordScanDiagnostics(float now, long ticks, bool failed)
	{
		_diagnosticScans++;
		if (failed)
		{
			_diagnosticFailures++;
		}
		_scanLastTicks = ticks;
		_scanTotalTicks += ticks;
		_scanMaxTicks = Math.Max(_scanMaxTicks, ticks);
		_peerTotalTicks += _peerLastTicks;
		_peerMaxTicks = Math.Max(_peerMaxTicks, _peerLastTicks);
		_lookupTotalTicks += _lookupLastTicks;
		_lookupMaxTicks = Math.Max(_lookupMaxTicks, _lookupLastTicks);
		_totalPeers += _lastPeers;
		_totalZdoLookups += _lastZdoLookups;
		_totalDeadReads += _lastDeadReads;
		if (!(now < _nextDiagnostics))
		{
			Log.LogInfo((object)string.Format(CultureInfo.InvariantCulture, "Death scan diagnostics ({19:F2} min): scans={0}, failed={1}; peers last/avg={2}/{3:F1}, tracked={4}; full scan ms last/avg/max={5:F4}/{6:F4}/{7:F4}; peer loop ms last/avg/max={8:F4}/{9:F4}/{10:F4}; ZDO lookup + dead flag ms last/avg/max={11:F4}/{12:F4}/{13:F4}; ZDO lookups last/total={14}/{15}, dead flag reads last/total={16}/{17}, missing ZDO last={18}.", _diagnosticScans, _diagnosticFailures, _lastPeers, (double)_totalPeers / (double)_diagnosticScans, _characters.Count, Milliseconds(_scanLastTicks), Milliseconds(_scanTotalTicks) / (double)_diagnosticScans, Milliseconds(_scanMaxTicks), Milliseconds(_peerLastTicks), Milliseconds(_peerTotalTicks) / (double)_diagnosticScans, Milliseconds(_peerMaxTicks), Milliseconds(_lookupLastTicks), Milliseconds(_lookupTotalTicks) / (double)_diagnosticScans, Milliseconds(_lookupMaxTicks), _lastZdoLookups, _totalZdoLookups, _lastDeadReads, _totalDeadReads, _lastMissingZdos, _diagnosticsIntervalMinutes.Value));
			ResetDiagnostics(now);
		}
	}

	private void Update()
	{
		EnsureReasonRpc();
		SendMachineName();
		_announcementUi?.Tick(Mathf.Clamp(_bannerSeconds.Value, 1f, 60f));
		if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || ZDOMan.instance == null || !Enabled.Value)
		{
			return;
		}
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		if (realtimeSinceStartup < _nextScan)
		{
			return;
		}
		_nextScan = realtimeSinceStartup + _scanIntervalSeconds.Value;
		_lastPeers = (_lastZdoLookups = (_lastDeadReads = (_lastMissingZdos = 0)));
		_peerLastTicks = (_lookupLastTicks = 0L);
		bool value = _diagnosticsEnabled.Value;
		long num = (value ? Stopwatch.GetTimestamp() : 0);
		bool failed = false;
		try
		{
			ScanCharacters(realtimeSinceStartup);
		}
		catch (Exception ex)
		{
			failed = true;
			if (realtimeSinceStartup >= _nextErrorLog)
			{
				_nextErrorLog = realtimeSinceStartup + 10f;
				Log.LogError((object)("Server death scan failed: " + ex));
			}
		}
		finally
		{
			if (value)
			{
				long ticks = Stopwatch.GetTimestamp() - num;
				RecordScanDiagnostics(realtimeSinceStartup, ticks, failed);
			}
		}
	}

	private void ScanCharacters(float now)
	{
		//IL_0048: Unknown result type (might be due to invalid IL or missing references)
		//IL_004d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0079: Unknown result type (might be due to invalid IL or missing references)
		//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
		//IL_01b1: Unknown result type (might be due to invalid IL or missing references)
		//IL_0313: Unknown result type (might be due to invalid IL or missing references)
		//IL_0318: Unknown result type (might be due to invalid IL or missing references)
		//IL_0320: Unknown result type (might be due to invalid IL or missing references)
		//IL_01f2: Unknown result type (might be due to invalid IL or missing references)
		//IL_02a8: Unknown result type (might be due to invalid IL or missing references)
		bool value = _diagnosticsEnabled.Value;
		long num = (value ? Stopwatch.GetTimestamp() : 0);
		try
		{
			foreach (ZNetPeer peer in ZNet.instance.GetPeers())
			{
				_lastPeers++;
				if (peer.m_characterID == ZDOID.None)
				{
					continue;
				}
				string peerHost = DeathBanUtil.GetPeerHost(peer);
				if (string.IsNullOrWhiteSpace(peerHost))
				{
					continue;
				}
				if (!_characters.TryGetValue(peer.m_characterID, out var value2))
				{
					value2 = new CharacterWatch
					{
						Sender = peer.m_uid,
						PlayerName = DeathBanUtil.GetPeerName(peer),
						Host = peerHost
					};
					_characters.Add(peer.m_characterID, value2);
					if (_machineCheckEnabled.Value && !_machineSeen.ContainsKey(peer.m_uid) && !_machinePending.ContainsKey(peer.m_uid))
					{
						_machinePending[peer.m_uid] = new PendingMachine
						{
							PlayerName = value2.PlayerName,
							Host = peerHost,
							Since = now
						};
					}
				}
				if (!(value2.Host != peerHost))
				{
					value2.LastSeen = now;
				}
			}
		}
		finally
		{
			if (value)
			{
				_peerLastTicks = Stopwatch.GetTimestamp() - num;
			}
		}
		List<ZDOID> list = new List<ZDOID>();
		foreach (KeyValuePair<ZDOID, CharacterWatch> character in _characters)
		{
			CharacterWatch value3 = character.Value;
			if (now - value3.LastSeen > 30f)
			{
				list.Add(character.Key);
			}
			else
			{
				if (value3.Accepted)
				{
					continue;
				}
				bool flag = false;
				long num2 = (value ? Stopwatch.GetTimestamp() : 0);
				try
				{
					_lastZdoLookups++;
					ZDO zDO = ZDOMan.instance.GetZDO(character.Key);
					if (zDO == null)
					{
						_lastMissingZdos++;
					}
					else
					{
						_lastDeadReads++;
						flag = zDO.GetBool(DeathBanUtil.DeadKey, false);
					}
				}
				finally
				{
					if (value)
					{
						_lookupLastTicks += Stopwatch.GetTimestamp() - num2;
					}
				}
				if (flag)
				{
					bool flag2 = !BanAdmins.Value && DeathBanUtil.IsAdmin(value3.Host);
					value3.Accepted = true;
					Log.LogInfo((object)$"Server death confirmed: player='{Clean(value3.PlayerName)}', id='{Clean(value3.Host)}', character={character.Key}, admin_exempt={flag2}.");
					((MonoBehaviour)this).StartCoroutine(ProcessDeath(value3.Sender, value3.PlayerName, value3.Host, flag2));
				}
			}
		}
		foreach (ZDOID item in list)
		{
			_characters.Remove(item);
		}
		if (_machinePending.Count > 0)
		{
			List<long> list2 = null;
			foreach (KeyValuePair<long, PendingMachine> item2 in _machinePending)
			{
				if (now - item2.Value.Since > 30f)
				{
					if (list2 == null)
					{
						list2 = new List<long>();
					}
					list2.Add(item2.Key);
				}
			}
			if (list2 != null)
			{
				foreach (long item3 in list2)
				{
					PendingMachine pendingMachine = _machinePending[item3];
					_machinePending.Remove(item3);
					_machineSeen[item3] = null;
					string verdict = ((DeathBanUtil.FindPeer(item3) == null) ? "имя ПК не получено (игрок уже вышел)" : "имя ПК не получено (нет мода 1.1.13 или клиент изменён)");
					RecordMachine(Clean(pendingMachine.PlayerName), Clean(pendingMachine.Host), "unknown", verdict, match: false, missing: true);
				}
			}
		}
		List<long> list3 = new List<long>();
		foreach (KeyValuePair<long, ReasonReport> report in _reports)
		{
			if (now - report.Value.ReceivedAt > 15f)
			{
				list3.Add(report.Key);
			}
		}
		foreach (long item4 in list3)
		{
			_reports.Remove(item4);
		}
	}

	internal void EnsureReasonRpc()
	{
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance != null && instance != _registeredRouter)
		{
			((MonoBehaviour)this).StopAllCoroutines();
			_announcementUi?.Dispose();
			_announcementUi = null;
			_characters.Clear();
			_nextScan = 0f;
			ResetDiagnostics(Time.realtimeSinceStartup);
			_reports.Clear();
			_machineSeen.Clear();
			_machinePending.Clear();
			instance.Register<string>("xtasy_DeathBan_Machine_v1", (Action<long, string>)ReceiveMachine);
			instance.Register<string>("xtasy_DeathBan_Reason_v1", (Action<long, string>)ReceiveReason);
			instance.Register<string, bool, bool>("xtasy_DeathBan_Announcement_v1", (Action<long, string, bool, bool>)ReceiveAnnouncement);
			_registeredRouter = instance;
			Log.LogInfo((object)"Death reason RPC registered (reason only; bans require server ZDO dead flag).");
		}
	}

	private void ReceiveReason(long sender, string reason)
	{
		if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || !Enabled.Value)
		{
			return;
		}
		try
		{
			ZNetPeer val = DeathBanUtil.FindPeer(sender);
			if (val == null)
			{
				Log.LogWarning((object)$"Death report rejected: no connected peer for sender={sender}.");
				return;
			}
			_reports[sender] = new ReasonReport
			{
				Host = DeathBanUtil.GetPeerHost(val),
				Reason = Clean(reason),
				ReceivedAt = Time.realtimeSinceStartup
			};
			Log.LogInfo((object)$"Death reason received: sender={sender}; awaiting server ZDO confirmation.");
		}
		catch (Exception ex)
		{
			Log.LogError((object)("Death report processing failed: " + ex));
		}
	}

	internal void ReportLocalDeath(Player player)
	{
		if ((Object)(object)ZNet.instance == (Object)null || (Object)(object)player == (Object)null || (Object)(object)player != (Object)(object)Player.m_localPlayer)
		{
			return;
		}
		try
		{
			if (ZNet.instance.IsServer())
			{
				if (Enabled.Value)
				{
					string reason = DeathBanUtil.DescribeLastHit(player);
					WriteDeath(player.GetPlayerName(), "local-host", reason, "local host (ban exempt)", exempt: true);
					AnnounceDeath(player.GetPlayerName(), reason);
					NotifyDeath(player.GetPlayerName(), "local-host", reason, "Хозяин локального мира — освобождён от бана", DateTimeOffset.Now);
				}
				return;
			}
			Log.LogInfo((object)("Client death detected: " + Clean(player.GetPlayerName())));
			EnsureReasonRpc();
			if (ZRoutedRpc.instance == null)
			{
				Log.LogWarning((object)"Death report not sent: router unavailable.");
				return;
			}
			long serverPeerId = DeathBanUtil.GetServerPeerId(ZRoutedRpc.instance);
			if (serverPeerId == 0L)
			{
				Log.LogWarning((object)"Death report not sent: server peer unavailable.");
				return;
			}
			ZRoutedRpc.instance.InvokeRoutedRPC(serverPeerId, "xtasy_DeathBan_Reason_v1", new object[1] { DeathBanUtil.DescribeLastHit(player) });
			Log.LogInfo((object)("Death report sent: server=" + serverPeerId));
		}
		catch (Exception ex)
		{
			Log.LogWarning((object)("Could not report death reason: " + ex.Message));
		}
	}

	internal IEnumerator ProcessDeath(long sender, string playerName, string host, bool exempt)
	{
		float start = Time.realtimeSinceStartup;
		DateTimeOffset diedAt = DateTimeOffset.Now;
		float num = Math.Min(1f, Mathf.Max(0f, KickDelaySeconds.Value));
		if (num > 0f)
		{
			yield return (object)new WaitForSecondsRealtime(num);
		}
		string reason = "неизвестна (клиент не передал данные)";
		string source = "server ZDO; reason=unavailable";
		if (_reports.TryGetValue(sender, out var value) && value.Host == host && Time.realtimeSinceStartup - value.ReceivedAt <= 15f)
		{
			reason = value.Reason;
			source = "server ZDO; reason=client report";
		}
		_reports.Remove(sender);
		WriteDeath(playerName, host, reason, source, exempt);
		AnnounceDeath(playerName, reason);
		if (exempt)
		{
			NotifyDeath(playerName, host, reason, "Администратор — освобождён от бана", diedAt);
			yield break;
		}
		if (!Enabled.Value)
		{
			NotifyDeath(playerName, host, reason, "Бан не применён: мод отключён", diedAt);
			yield break;
		}
		float num2 = Mathf.Max(0f, KickDelaySeconds.Value) - (Time.realtimeSinceStartup - start);
		if (num2 > 0f)
		{
			yield return (object)new WaitForSecondsRealtime(num2);
		}
		if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer())
		{
			yield break;
		}
		if (!Enabled.Value)
		{
			NotifyDeath(playerName, host, reason, "Бан не применён: мод отключён", diedAt);
			yield break;
		}
		if (!BanAdmins.Value && DeathBanUtil.IsAdmin(host))
		{
			NotifyDeath(playerName, host, reason, "Администратор — освобождён от бана", diedAt);
			yield break;
		}
		try
		{
			DeathBanUtil.InvokeZNetUserAction("Ban", host);
			NotifyDeath(playerName, host, reason, "Забанен", diedAt);
			try
			{
				DeathBanUtil.InvokeZNetUserAction("Kick", host, throwIfMissing: false);
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("Death-ban applied, but fallback kick failed: " + ex.GetType().Name));
			}
			Log.LogWarning((object)("Death-ban applied: player='" + playerName + "', id='" + host + "'."));
		}
		catch (Exception arg)
		{
			Log.LogError((object)$"Failed to death-ban '{playerName}' ({host}): {arg}");
			NotifyDeath(playerName, host, reason, "Ошибка применения бана — проверьте лог сервера", diedAt);
		}
	}

	private void SendMachineName()
	{
		if (!_machineCheckEnabled.Value || (Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer())
		{
			return;
		}
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance == null || (Object)(object)Player.m_localPlayer == (Object)null || instance == _machineSentRouter)
		{
			return;
		}
		try
		{
			long serverPeerId = DeathBanUtil.GetServerPeerId(instance);
			if (serverPeerId != 0L)
			{
				instance.InvokeRoutedRPC(serverPeerId, "xtasy_DeathBan_Machine_v1", new object[1] { MachineLabel });
				_machineSentRouter = instance;
			}
		}
		catch (Exception ex)
		{
			_machineSentRouter = instance;
			Log.LogWarning((object)("Could not send machine name: " + ex.Message));
		}
	}

	private void ReceiveMachine(long sender, string machine)
	{
		if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || !_machineCheckEnabled.Value)
		{
			return;
		}
		try
		{
			ZNetPeer val = DeathBanUtil.FindPeer(sender);
			if (val == null)
			{
				Log.LogWarning((object)$"Machine name rejected: no connected peer for sender={sender}.");
				return;
			}
			string text = Clean(machine).Trim();
			if (text.Length > 64)
			{
				text = text.Substring(0, 64);
			}
			string text2 = Clean(DeathBanUtil.GetPeerName(val));
			string text3 = Clean(DeathBanUtil.GetPeerHost(val));
			if (_machineSeen.TryGetValue(sender, out var value) && value != null)
			{
				if (!string.Equals(value, text, StringComparison.OrdinalIgnoreCase))
				{
					Log.LogWarning((object)("Machine name changed mid-session: player='" + text2 + "', id='" + text3 + "', first='" + value + "', now='" + text + "'."));
				}
			}
			else
			{
				_machineSeen[sender] = text;
				_machinePending.Remove(sender);
				bool flag = IsBlockedMachine(text);
				RecordMachine(text2, text3, text, flag ? _machineMatchText.Value : _machineNoMatchText.Value, flag, missing: false);
			}
		}
		catch (Exception ex)
		{
			Log.LogError((object)("Machine name processing failed: " + ex));
		}
	}

	private bool IsBlockedMachine(string machine)
	{
		string value = _blockedMachines.Value;
		if (string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(machine))
		{
			return false;
		}
		string[] array = value.Split(new char[2] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
		for (int i = 0; i < array.Length; i++)
		{
			if (string.Equals(array[i].Trim(), machine.Trim(), StringComparison.OrdinalIgnoreCase))
			{
				return true;
			}
		}
		return false;
	}

	private static void RecordMachine(string player, string host, string machine, string verdict, bool match, bool missing)
	{
		string text = verdict + ": player='" + player + "', id='" + host + "', machine='" + machine + "'.";
		if (match || missing)
		{
			Log.LogWarning((object)text);
		}
		else
		{
			Log.LogInfo((object)text);
		}
		try
		{
			string text2 = Path.Combine(Paths.ConfigPath, "ValheimDeathBan");
			Directory.CreateDirectory(text2);
			string text3 = $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss zzz} | player={player} | id={host} | machine={machine} | verdict={verdict}";
			File.AppendAllText(Path.Combine(text2, "machines.txt"), text3 + Environment.NewLine, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
		}
		catch (Exception ex)
		{
			Log.LogError((object)("Failed to write machine journal: " + ex));
		}
	}

	private static string GetMachineLabel()
	{
		try
		{
			return Environment.MachineName;
		}
		catch
		{
			return "unknown";
		}
	}

	private static string Clean(string text)
	{
		if (text == null)
		{
			return "unknown";
		}
		text = text.Replace('\r', ' ').Replace('\n', ' ').Replace('\t', ' ');
		if (text.Length <= 512)
		{
			return text;
		}
		return text.Substring(0, 512);
	}

	private static void WriteDeath(string name, string host, string reason, string source, bool exempt)
	{
		try
		{
			string text = Path.Combine(Paths.ConfigPath, "ValheimDeathBan");
			Directory.CreateDirectory(text);
			string text2 = $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss zzz} | player={Clean(name)} | id={Clean(host)} | reason={Clean(reason)} | source={source} | admin_exempt={exempt}";
			File.AppendAllText(Path.Combine(text, "deaths.txt"), text2 + Environment.NewLine, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
			Log.LogInfo((object)text2);
		}
		catch (Exception ex)
		{
			Log.LogError((object)("Failed to write death journal: " + ex));
		}
	}

	private void Awake()
	{
		//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ae: Expected O, but got Unknown
		//IL_011a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0124: Expected O, but got Unknown
		//IL_02b5: Unknown result type (might be due to invalid IL or missing references)
		//IL_02bf: Expected O, but got Unknown
		Instance = this;
		Log = ((BaseUnityPlugin)this).Logger;
		Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Ban a player from this server after their character dies.");
		KickDelaySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "KickDelaySeconds", 3f, "Seconds to wait after death before disconnecting the player.");
		BanAdmins = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "BanAdmins", false, "If false, players present in adminlist.txt are exempt from death bans.");
		_scanIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "ScanIntervalSeconds", 0.25f, new ConfigDescription("Seconds between server player/death scans. Larger values delay death detection. Range: 0.05-30 seconds.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 30f), Array.Empty<object>()));
		_scanIntervalSeconds.SettingChanged += OnScanIntervalChanged;
		_diagnosticsEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "Enabled", true, "Enable server death-scan timing and periodic diagnostic logs.");
		_diagnosticsIntervalMinutes = ((BaseUnityPlugin)this).Config.Bind<float>("Diagnostics", "LogIntervalMinutes", 15f, new ConfigDescription("Minutes between server scan diagnostic logs. Range: 0.1-1440 minutes.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 1440f), Array.Empty<object>()));
		_diagnosticsEnabled.SettingChanged += OnDiagnosticsChanged;
		_diagnosticsIntervalMinutes.SettingChanged += OnDiagnosticsChanged;
		ResetDiagnostics(Time.realtimeSinceStartup);
		_discordEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Discord", "Enabled", false, "Send death notifications to Discord from the server. Sends names and account IDs.");
		_webhookUrl = ((BaseUnityPlugin)this).Config.Bind<string>("Discord", "WebhookUrl", "", "Discord webhook URL. Server-only secret; never synced to clients. Empty disables sending.");
		_discord = new DiscordDeathNotifier(Log);
		_announceChat = ((BaseUnityPlugin)this).Config.Bind<bool>("Announcements", "Chat", true, "Announce all confirmed deaths in shout-style chat (/s) using the PvP Tweaks rendering pattern. Clients need 1.1.6.");
		_announceBanner = ((BaseUnityPlugin)this).Config.Bind<bool>("Announcements", "Banner", true, "Broadcast a large red Norse death banner. Clients need Death Ban 1.1.6 or newer.");
		_bannerSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Announcements", "BannerSeconds", 8f, "Client banner duration in seconds, clamped to 1-60. Multiple deaths are queued.");
		_machineCheckEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("MachineCheck", "Enabled", true, "On world entry the client sends its computer name to the server; the server logs it and compares it with BlockedMachineNames. Clients need Death Ban 1.1.13 or newer.");
		_blockedMachines = ((BaseUnityPlugin)this).Config.Bind<string>("MachineCheck", "BlockedMachineNames", "DESKTOP-84C56UO", "Computer names to flag, separated by commas or semicolons. Case-insensitive.");
		_machineMatchText = ((BaseUnityPlugin)this).Config.Bind<string>("MachineCheck", "MatchText", "гандон обнаружен", "Log text when the computer name matches BlockedMachineNames.");
		_machineNoMatchText = ((BaseUnityPlugin)this).Config.Bind<string>("MachineCheck", "NoMatchText", "норм тип", "Log text when the computer name does not match.");
		_harmony = new Harmony("com.xtasy.ValheimDeathBan");
		_harmony.PatchAll();
		Log.LogInfo((object)"Valheim Death Ban 1.1.13 loaded. Server ZDO death monitoring; client reports provide reasons only.");
	}

	private void OnDiagnosticsChanged(object sender, EventArgs args)
	{
		ResetDiagnostics(Time.realtimeSinceStartup);
	}

	private void OnScanIntervalChanged(object sender, EventArgs args)
	{
		_nextScan = 0f;
	}

	private void OnDestroy()
	{
		if (_scanIntervalSeconds != null)
		{
			_scanIntervalSeconds.SettingChanged -= OnScanIntervalChanged;
		}
		if (_diagnosticsEnabled != null)
		{
			_diagnosticsEnabled.SettingChanged -= OnDiagnosticsChanged;
		}
		if (_diagnosticsIntervalMinutes != null)
		{
			_diagnosticsIntervalMinutes.SettingChanged -= OnDiagnosticsChanged;
		}
		_announcementUi?.Dispose();
		_discord?.Dispose();
		Harmony harmony = _harmony;
		if (harmony != null)
		{
			harmony.UnpatchSelf();
		}
	}
}
internal static class DeathBanUtil
{
	private const BindingFlags AnyInstance = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

	internal static readonly int DeadKey = ZDOVars.s_dead;

	private static readonly MethodInfo ServerPeerIdMethod = typeof(ZRoutedRpc).GetMethod("GetServerPeerID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);

	internal static long GetServerPeerId(ZRoutedRpc router)
	{
		if (ServerPeerIdMethod == null)
		{
			throw new MissingMethodException(typeof(ZRoutedRpc).FullName, "GetServerPeerID()");
		}
		return (long)ServerPeerIdMethod.Invoke(router, null);
	}

	internal static string DescribeLastHit(Player player)
	{
		object? obj = typeof(Character).GetField("m_lastHit", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(player);
		HitData val = (HitData)((obj is HitData) ? obj : null);
		if (val == null)
		{
			return "неизвестна (нет последнего удара)";
		}
		string text = ((object)Unsafe.As<HitType, HitType>(ref val.m_hitType)/*cast due to .constrained prefix*/).ToString();
		string text2 = text switch
		{
			"EnemyHit" => "атака существа", 
			"PlayerHit" => "атака игрока", 
			"Fall" => "падение", 
			"Drowning" => "утопление", 
			"Burning" => "горение", 
			"Freezing" => "замерзание", 
			"Poisoned" => "отравление", 
			"Water" => "урон от воды", 
			"Smoke" => "дым", 
			"EdgeOfWorld" => "край мира", 
			"Impact" => "удар объектом", 
			"Cart" => "телега", 
			"Tree" => "дерево", 
			"Self" => "урон себе", 
			"Structural" => "строительная конструкция", 
			"Turret" => "баллиста", 
			"Boat" => "корабль", 
			"Stalagtite" => "сталактит", 
			"Catapult" => "катапульта", 
			"CinderFire" => "пепельный огонь", 
			"AshlandsOcean" => "кипящий океан Пепельных земель", 
			"AshlandsLava" => "лава", 
			"Incinerator" => "уничтожитель предметов", 
			"DrawBridge" => "подъёмный мост", 
			_ => "неизвестная причина", 
		};
		Character attacker = val.GetAttacker();
		Player val2 = (Player)(object)((attacker is Player) ? attacker : null);
		string text3 = ((val2 != null) ? val2.GetPlayerName() : (((Object)(object)attacker != (Object)null) ? attacker.m_name : null));
		return text2 + " [" + text + "]" + (string.IsNullOrEmpty(text3) ? "" : ("; attacker=" + text3));
	}

	internal static ZNetPeer FindPeer(long sender)
	{
		if ((Object)(object)ZNet.instance == (Object)null)
		{
			return null;
		}
		MethodInfo method = typeof(ZNet).GetMethod("GetPeer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(long) }, null);
		if (method != null)
		{
			object? obj = method.Invoke(ZNet.instance, new object[1] { sender });
			return (ZNetPeer)((obj is ZNetPeer) ? obj : null);
		}
		if (!(typeof(ZNet).GetField("m_peers", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(ZNet.instance) is IEnumerable enumerable))
		{
			return null;
		}
		foreach (object item in enumerable)
		{
			ZNetPeer val = (ZNetPeer)((item is ZNetPeer) ? item : null);
			if (val != null)
			{
				FieldInfo field = typeof(ZNetPeer).GetField("m_uid", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (field != null && Convert.ToInt64(field.GetValue(val)) == sender)
				{
					return val;
				}
			}
		}
		return null;
	}

	internal static string GetPeerName(ZNetPeer peer)
	{
		return typeof(ZNetPeer).GetField("m_playerName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(peer)?.ToString() ?? "unknown";
	}

	internal static string GetPeerHost(ZNetPeer peer)
	{
		if (peer == null)
		{
			return null;
		}
		object obj = typeof(ZNetPeer).GetField("m_socket", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(peer);
		return obj?.GetType().GetMethod("GetHostName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null)?.Invoke(obj, null)?.ToString();
	}

	internal static bool IsAdmin(string host)
	{
		if ((Object)(object)ZNet.instance == (Object)null || string.IsNullOrWhiteSpace(host))
		{
			return false;
		}
		object obj = typeof(ZNet).GetField("m_adminList", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(ZNet.instance);
		if (obj == null)
		{
			return false;
		}
		MethodInfo method = obj.GetType().GetMethod("Contains", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null);
		if (method != null)
		{
			return (bool)method.Invoke(obj, new object[1] { host });
		}
		return false;
	}

	internal static void InvokeZNetUserAction(string methodName, string user, bool throwIfMissing = true)
	{
		MethodInfo method = typeof(ZNet).GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null);
		if (method == null)
		{
			if (throwIfMissing)
			{
				throw new MissingMethodException(typeof(ZNet).FullName, methodName + "(string)");
			}
		}
		else
		{
			method.Invoke(ZNet.instance, new object[1] { user });
		}
	}
}
[HarmonyPatch(typeof(Player), "OnDeath")]
internal static class PlayerLocalDeathPatch
{
	private static void Prefix(Player __instance)
	{
		ValheimDeathBanPlugin.Instance.ReportLocalDeath(__instance);
	}
}