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