using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Landoria.SharedLib; using TMPro; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Landoria.AfkDetector")] [assembly: AssemblyDescription("Disconnects inactive Valheim players with a clear reason.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Landoria")] [assembly: AssemblyProduct("Landoria.AfkDetector")] [assembly: AssemblyCopyright("Copyright © 2026 End3rbyte")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("7B88B753-913F-4936-88DF-173D3F373E9B")] [assembly: AssemblyFileVersion("1.0.1")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = "")] [assembly: AssemblyVersion("1.0.1.16295")] namespace Landoria.AfkDetector { internal sealed class ActivityMonitor { private sealed class PlayerActivity { internal Vector3 Position; internal float LastActivityAt; internal bool DisconnectRequested; internal PlayerActivity(Vector3 position, float now) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) Position = position; LastActivityAt = now; } } private readonly Dictionary _players = new Dictionary(); private readonly Action _disconnect; private float _timeoutSeconds; private float _movementToleranceSquared; internal ActivityMonitor(float timeoutSeconds, float movementTolerance, Action disconnect) { _disconnect = disconnect; Configure(timeoutSeconds, movementTolerance); } internal void Configure(float timeoutSeconds, float movementTolerance) { _timeoutSeconds = timeoutSeconds; _movementToleranceSquared = movementTolerance * movementTolerance; } internal void Update(List peers, float now) { HashSet hashSet = new HashSet(); foreach (ZNetPeer peer in peers) { if (peer.IsReady()) { hashSet.Add(peer.m_uid); UpdatePeer(peer, now); } } RemoveDisconnected(hashSet); } internal void RecordChat(long peerId, float now) { if (_players.TryGetValue(peerId, out var value)) { value.LastActivityAt = now; } } private void UpdatePeer(ZNetPeer peer, float now) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) if (!_players.TryGetValue(peer.m_uid, out var value)) { _players[peer.m_uid] = new PlayerActivity(peer.GetRefPos(), now); } else if (HasMoved(value.Position, peer.GetRefPos())) { value.Position = peer.GetRefPos(); value.LastActivityAt = now; } else if (!value.DisconnectRequested && now - value.LastActivityAt >= _timeoutSeconds) { value.DisconnectRequested = true; _disconnect(peer); } } private bool HasMoved(Vector3 previous, Vector3 current) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) Vector3 val = current - previous; return ((Vector3)(ref val)).sqrMagnitude >= _movementToleranceSquared; } private void RemoveDisconnected(HashSet connected) { List list = new List(); foreach (long key in _players.Keys) { if (!connected.Contains(key)) { list.Add(key); } } foreach (long item in list) { _players.Remove(item); } } } [BepInPlugin("Landoria.AfkDetector", "Landoria.AfkDetector", "1.0.1")] public sealed class AfkDetectorPlugin : LandoriaPlugin { internal const string DisconnectReasonRpc = "Landoria_AfkDisconnectReason"; private const string PluginGuid = "Landoria.AfkDetector"; private const string PluginName = "Landoria.AfkDetector"; private const string PluginVersion = "1.0.1"; private const int DefaultTimeoutMinutes = 30; private const string TimeoutArgument = "--afktimeout"; private const float DefaultMovementTolerance = 0.75f; private const float ScanIntervalSeconds = 2f; private ConfigEntry _timeoutMinutes; private int? _commandLineTimeoutMinutes; private ConfigEntry _movementTolerance; private ActivityMonitor _monitor; private float _nextScan; internal static AfkDetectorPlugin Instance { get; private set; } internal static ModLog Log { get; private set; } private void Awake() { Instance = this; Log = InitializePlugin("Landoria.AfkDetector"); BindConfiguration(); Log.LogInfo("Landoria.AfkDetector 1.0.1 is loaded."); } private void BindConfiguration() { _timeoutMinutes = ((BaseUnityPlugin)this).Config.Bind("Detection", "TimeoutMinutes", 30, "Minutes without movement or chat before the server disconnects a player."); _commandLineTimeoutMinutes = ReadCommandLineTimeout(); _movementTolerance = ((BaseUnityPlugin)this).Config.Bind("Detection", "MovementToleranceMeters", 0.75f, "Minimum distance that resets the inactivity timer and filters position jitter."); } private void Update() { if (IsReadyServer() && !(Time.unscaledTime < _nextScan)) { _nextScan = Time.unscaledTime + 2f; EnsureMonitor().Update(ZNet.instance.GetPeers(), Time.unscaledTime); } } private ActivityMonitor EnsureMonitor() { float timeoutSeconds = (float)Mathf.Max(1, EffectiveTimeoutMinutes()) * 60f; float movementTolerance = Mathf.Max(0.1f, _movementTolerance.Value); if (_monitor == null) { _monitor = new ActivityMonitor(timeoutSeconds, movementTolerance, DisconnectPlayer); } else { _monitor.Configure(timeoutSeconds, movementTolerance); } return _monitor; } private int EffectiveTimeoutMinutes() { return _commandLineTimeoutMinutes ?? _timeoutMinutes.Value; } private static int? ReadCommandLineTimeout() { string[] commandLineArgs = Environment.GetCommandLineArgs(); for (int i = 0; i < commandLineArgs.Length; i++) { if (string.Equals(commandLineArgs[i], "--afktimeout", StringComparison.OrdinalIgnoreCase)) { return ParseCommandLineTimeout(commandLineArgs, i); } } return null; } private static int? ParseCommandLineTimeout(string[] arguments, int index) { if (index + 1 < arguments.Length && int.TryParse(arguments[index + 1], out var result) && result >= 1) { Log.LogInfo(string.Format("Received command-line switch: {0} {1}.", "--afktimeout", result)); return result; } Log.LogWarning("Invalid --afktimeout value; using the BepInEx configuration."); return null; } internal void RecordChat(long peerId) { if (IsReadyServer()) { EnsureMonitor().RecordChat(peerId, Time.unscaledTime); } } private static bool IsReadyServer() { if ((Object)(object)ZNet.instance != (Object)null) { return ZNet.instance.IsServer(); } return false; } private static void DisconnectPlayer(ZNetPeer peer) { string text = "Disconnected due to inactivity."; peer.m_rpc.Invoke("Landoria_AfkDisconnectReason", new object[1] { text }); ZNet.instance.Kick(peer.m_socket.GetHostName()); Log.LogInfo("Disconnected inactive player " + peer.m_playerName + "."); } private void OnDestroy() { Log?.LogInfo("Landoria.AfkDetector 1.0.1 is unloaded."); ShutdownPlugin(); _monitor = null; Instance = null; Log = null; } } [HarmonyPatch(typeof(ZRoutedRpc), "RPC_RoutedRPC")] internal static class ChatActivityPatch { private static readonly int ChatMessageHash = StringExtensionMethods.GetStableHashCode("ChatMessage"); private static readonly int SayHash = StringExtensionMethods.GetStableHashCode("Say"); private static readonly int GroupRequestHash = StringExtensionMethods.GetStableHashCode("Landoria_Social_GroupRequest"); private static void Prefix(ZPackage pkg) { if ((Object)(object)AfkDetectorPlugin.Instance == (Object)null || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } try { RoutedRPCData val = ReadRoutedData(pkg); if (ContainsChatMessage(val)) { AfkDetectorPlugin.Instance.RecordChat(val.m_senderPeerID); } } catch (Exception ex) { AfkDetectorPlugin.Log.LogDebug("Ignored unreadable chat activity: " + ex.Message); } } private static RoutedRPCData ReadRoutedData(ZPackage source) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Expected O, but got Unknown ZPackage val = new ZPackage(source.GetArray()); RoutedRPCData val2 = new RoutedRPCData(); val2.Deserialize(val); return val2; } private static bool ContainsChatMessage(RoutedRPCData data) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown ZPackage val = new ZPackage(data.m_parameters.GetArray()); if (data.m_methodHash == ChatMessageHash) { return ReadChatMessage(val, hasPosition: true); } if (data.m_methodHash == SayHash) { return ReadChatMessage(val, hasPosition: false); } if (data.m_methodHash == GroupRequestHash) { return ReadGroupChat(val); } return false; } private static bool ReadChatMessage(ZPackage package, bool hasPosition) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) if (hasPosition) { package.ReadVector3(); } package.ReadInt(); package.ReadString(); package.ReadString(); return !string.IsNullOrWhiteSpace(package.ReadString()); } private static bool ReadGroupChat(ZPackage parameters) { ZPackage val = parameters.ReadPackage(); string text = val.ReadString(); val.ReadLong(); val.ReadString(); if (text == "chat") { return !string.IsNullOrWhiteSpace(val.ReadString()); } return false; } } internal static class ClientDisconnectReason { private static string _pending; internal static void Receive(ZRpc rpc, string message) { _pending = message; } internal static bool TryTake(out string message) { message = _pending; _pending = null; return !string.IsNullOrWhiteSpace(message); } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] internal static class ClientConnectionPatch { private static void Postfix(ZNet __instance, ZNetPeer peer) { if (!__instance.IsServer()) { peer.m_rpc.Register("Landoria_AfkDisconnectReason", (Action)ClientDisconnectReason.Receive); } } } [HarmonyPatch(typeof(FejdStartup), "ShowConnectError")] internal static class ConnectionErrorPatch { private static void Postfix(ConnectionStatus statusOverride, TMP_Text ___m_connectionFailedError) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Invalid comparison between Unknown and I4 if ((int)(((int)statusOverride == 0) ? ZNet.GetConnectionStatus() : statusOverride) == 12 && ClientDisconnectReason.TryTake(out var message)) { ___m_connectionFailedError.text = message; } } } } namespace Landoria.SharedLib { public abstract class LandoriaPlugin : BaseUnityPlugin { private Harmony _harmony; private bool _patchesApplied; protected ModLog InitializePlugin(string pluginGuid) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown ModLog modLog = new ModLog(((BaseUnityPlugin)this).Logger); Version version = ((object)this).GetType().Assembly.GetName().Version; modLog.LogInfo($"AssemblyVersion: {version}."); _harmony = new Harmony(pluginGuid); PatchOwnNamespace(modLog); return modLog; } protected void PatchOwnNamespace(ModLog log) { if (_patchesApplied) { log.LogDebug("Harmony patches are already active; skipping registration."); return; } string text = ((object)this).GetType().Namespace; Type[] types = Assembly.GetExecutingAssembly().GetTypes(); foreach (Type type in types) { if (type.Namespace == text) { _harmony.CreateClassProcessor(type).Patch(); } } _patchesApplied = true; log.LogDebug("Harmony patches were applied for the plugin namespace."); } protected void ShutdownPlugin() { if (_patchesApplied) { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _patchesApplied = false; } } } public sealed class ModLog { private readonly ManualLogSource _logger; public ModLog(ManualLogSource logger) { _logger = logger; } public void LogFatal(object message) { Write((LogLevel)1, message); } public void LogError(object message) { Write((LogLevel)2, message); } public void LogWarning(object message) { Write((LogLevel)4, message); } public void LogMessage(object message) { Write((LogLevel)8, message); } public void LogInfo(object message) { Write((LogLevel)16, message); } public void LogDebug(object message) { Write((LogLevel)32, message); } public void Log(LogLevel level, object message) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) Write(level, message); } private void Write(LogLevel level, object message) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) string arg = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); _logger.Log(level, (object)$"[{arg}] {message}"); } } }