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 AfkDetector v1.0.1
Landoria.AfkDetector.dll
Decompiled 2 hours agousing 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<long, PlayerActivity> _players = new Dictionary<long, PlayerActivity>(); private readonly Action<ZNetPeer> _disconnect; private float _timeoutSeconds; private float _movementToleranceSquared; internal ActivityMonitor(float timeoutSeconds, float movementTolerance, Action<ZNetPeer> disconnect) { _disconnect = disconnect; Configure(timeoutSeconds, movementTolerance); } internal void Configure(float timeoutSeconds, float movementTolerance) { _timeoutSeconds = timeoutSeconds; _movementToleranceSquared = movementTolerance * movementTolerance; } internal void Update(List<ZNetPeer> peers, float now) { HashSet<long> hashSet = new HashSet<long>(); 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<long> connected) { List<long> list = new List<long>(); 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<int> _timeoutMinutes; private int? _commandLineTimeoutMinutes; private ConfigEntry<float> _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<int>("Detection", "TimeoutMinutes", 30, "Minutes without movement or chat before the server disconnects a player."); _commandLineTimeoutMinutes = ReadCommandLineTimeout(); _movementTolerance = ((BaseUnityPlugin)this).Config.Bind<float>("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<string>("Landoria_AfkDisconnectReason", (Action<ZRpc, string>)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}"); } } }