Decompiled source of AfkDetector v1.0.1

Landoria.AfkDetector.dll

Decompiled 2 hours ago
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<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}");
		}
	}
}