Decompiled source of AFKManager v0.3.1

plugins/AFKManager/AFKManager.dll

Decompiled a week ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Splatform;
using UnityEngine;

[assembly: AssemblyProduct("AFKManager")]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyTitle("AFKManager")]
[assembly: AssemblyFileVersion("0.3.0.0")]
[assembly: CompilationRelaxations(8)]
[assembly: AssemblyVersion("0.3.0.0")]
namespace AFKManager;

internal sealed class AFKConfig
{
	private const float DefaultAfkMinutes = 10f;

	private const float DefaultInterval = 5f;

	private const float DefaultMove = 0.5f;

	private const float DefaultLook = 10f;

	private readonly ConfigEntry<bool> _enabled;

	private readonly ConfigEntry<bool> _announceAfk;

	private readonly ConfigEntry<bool> _announceReturn;

	private readonly ConfigEntry<bool> _announceInChat;

	private readonly ConfigEntry<bool> _announceNotification;

	private readonly ConfigEntry<bool> _excludeFromSleep;

	private readonly ConfigEntry<bool> _excludeFromRaids;

	private readonly ConfigEntry<bool> _debug;

	private readonly ConfigEntry<float> _afkAfterMinutes;

	private readonly ConfigEntry<float> _checkInterval;

	private readonly ConfigEntry<float> _moveThreshold;

	private readonly ConfigEntry<float> _lookThreshold;

	private readonly ConfigEntry<string> _afkMessage;

	private readonly ConfigEntry<string> _returnMessage;

	private readonly ConfigEntry<string> _manualAfkEmote;

	private readonly ManualLogSource _log;

	private bool _warnedInvalid;

	internal bool Enabled => _enabled.Value;

	internal bool AnnounceAfk => _announceAfk.Value;

	internal bool AnnounceReturn => _announceReturn.Value;

	internal bool AnnounceInChat => _announceInChat.Value;

	internal bool AnnounceNotification => _announceNotification.Value;

	internal bool ExcludeAfkFromSleep => _excludeFromSleep.Value;

	internal bool ExcludeAfkFromRandomRaids => _excludeFromRaids.Value;

	internal bool DebugLogging => _debug.Value;

	internal string AfkMessage => _afkMessage.Value;

	internal string ReturnMessage => _returnMessage.Value;

	internal string ManualAfkEmote
	{
		get
		{
			string value = _manualAfkEmote.Value;
			if (value != null)
			{
				return value.Trim();
			}
			return "";
		}
	}

	internal float AfkAfterSeconds => Safe(_afkAfterMinutes, 10f, 0.1f, 1440f) * 60f;

	internal float CheckIntervalSeconds => Safe(_checkInterval, 5f, 1f, 60f);

	internal float MovementThresholdMeters => Safe(_moveThreshold, 0.5f, 0.05f, 10f);

	internal float LookThresholdDegrees => Safe(_lookThreshold, 10f, 1f, 180f);

	internal AFKConfig(ConfigFile cfg, ManualLogSource log)
	{
		//IL_0054: Unknown result type (might be due to invalid IL or missing references)
		//IL_005e: Expected O, but got Unknown
		//IL_008e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0098: Expected O, but got Unknown
		//IL_00c8: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d2: Expected O, but got Unknown
		//IL_0102: Unknown result type (might be due to invalid IL or missing references)
		//IL_010c: Expected O, but got Unknown
		_log = log;
		_enabled = cfg.Bind<bool>("General", "Enabled", true, "Master switch. When false AFKManager does nothing (no detection, no announcements, no sleep changes).");
		_afkAfterMinutes = cfg.Bind<float>("General", "AfkAfterMinutes", 10f, new ConfigDescription("Minutes without any detected activity before a player is marked AFK. Small values (e.g. 1) are useful for testing.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 1440f), new object[0]));
		_checkInterval = cfg.Bind<float>("General", "CheckIntervalSeconds", 5f, new ConfigDescription("How often (seconds) connected players are checked. The check is extremely cheap; 5 is recommended. The movement threshold below was validated for a 5 second interval.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f), new object[0]));
		_moveThreshold = cfg.Bind<float>("Detection", "MovementThresholdMeters", 0.5f, new ConfigDescription("Horizontal distance (meters) a player must move between two checks to count as active. 0.5 safely ignores physics drift (e.g. slowly sliding on a slope). On a ship, movement relative to the ship is used, so passengers standing still on a sailing ship do not count as moving.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 10f), new object[0]));
		_lookThreshold = cfg.Bind<float>("Detection", "LookThresholdDegrees", 10f, new ConfigDescription("How far (degrees) a player must turn their view to count as active.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 180f), new object[0]));
		_announceAfk = cfg.Bind<bool>("Announcements", "AnnounceAfk", true, "Tell all players when someone becomes AFK.");
		_announceReturn = cfg.Bind<bool>("Announcements", "AnnounceReturn", true, "Tell all players when someone is no longer AFK.");
		_announceInChat = cfg.Bind<bool>("Announcements", "AnnounceInChat", true, "Show announcements in the normal chat window (sender name: AFKManager). Works with vanilla clients.");
		_announceNotification = cfg.Bind<bool>("Announcements", "AnnounceNotification", true, "Show announcements as a small notification in the top-left message feed. Works with vanilla clients.");
		_afkMessage = cfg.Bind<string>("Announcements", "AfkMessage", "{player} is now AFK.", "Text shown when a player becomes AFK. {player} is replaced with the player's name.");
		_returnMessage = cfg.Bind<string>("Announcements", "ReturnMessage", "{player} is no longer AFK.", "Text shown when a player returns. {player} is replaced with the player's name.");
		_manualAfkEmote = cfg.Bind<string>("Manual AFK", "ManualAfkEmote", "rest", "Emote a player can use to mark THEMSELVES AFK right away, typed in the chat window as /<emote> (default: /rest). Starting it does not count as activity; any normal activity (moving, looking around, another emote, ...) makes the player active again. Valheim does not send made-up commands such as /afk to the server, but it does send emotes, so this works from a completely vanilla client on any platform. A player in a bed cannot emote, so sleeping never triggers it. Emote names: wave, sit, challenge, cheer, nonono, thumbsup, point, blowkiss, bow, cower, cry, despair, flex, comehere, headbang, kneel, laugh, roar, shrug, dance, relax, toast, rest, vibe, loveyou. Empty = disabled.");
		_excludeFromSleep = cfg.Bind<bool>("Gameplay", "ExcludeAfkFromSleep", true, "When true, AFK players who are not in bed no longer prevent the night from being skipped. At least one player must still be in bed. Requires a server restart to fully enable/disable the hook.");
		_excludeFromRaids = cfg.Bind<bool>("Raids", "ExcludeAfkFromRandomRaids", true, "When enabled, a new vanilla random raid will not begin in an AFK-only player area. If an active player is present, the raid is allowed. Existing raids are never cancelled when players become AFK. Boss, scripted and admin-started events are not affected. Requires a server restart to fully enable/disable the hook.");
		_debug = cfg.Bind<bool>("Logging", "DebugLogging", false, "Log activity reasons, AFK transitions and a short summary of every check. Leave off for normal play.");
	}

	private float Safe(ConfigEntry<float> entry, float fallback, float min, float max)
	{
		float value = entry.Value;
		if (float.IsNaN(value) || float.IsInfinity(value))
		{
			if (!_warnedInvalid)
			{
				_warnedInvalid = true;
				_log.LogWarning((object)("Config value '" + ((ConfigEntryBase)entry).Definition.Key + "' is not a valid number; using default " + fallback + "."));
			}
			return fallback;
		}
		if (!(value < min))
		{
			if (!(value > max))
			{
				return value;
			}
			return max;
		}
		return min;
	}
}
[BepInPlugin("torokal.afkmanager", "AFKManager", "0.3.0")]
public sealed class AFKManagerPlugin : BaseUnityPlugin
{
	public const string PluginGuid = "torokal.afkmanager";

	public const string PluginName = "AFKManager";

	public const string PluginVersion = "0.3.0";

	private static AfkService _service;

	private AFKConfig _config;

	private float _nextCheck;

	private bool _wasServer;

	public static bool IsAfk(long peerUid)
	{
		if (_service != null)
		{
			return _service.IsAfk(peerUid);
		}
		return false;
	}

	private void Awake()
	{
		_config = new AFKConfig(((BaseUnityPlugin)this).Config, ((BaseUnityPlugin)this).Logger);
		AnnouncementService announcer = new AnnouncementService(_config, ((BaseUnityPlugin)this).Logger);
		_service = new AfkService(_config, ((BaseUnityPlugin)this).Logger, announcer);
		SleepIntegration.TryEnable(_service, _config, ((BaseUnityPlugin)this).Logger);
		RaidIntegration.TryEnable(_service, _config, ((BaseUnityPlugin)this).Logger);
		((BaseUnityPlugin)this).Logger.LogInfo((object)"AFKManager 0.3.0 loaded (server-side only; does nothing on clients).");
	}

	private void Update()
	{
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		if (realtimeSinceStartup < _nextCheck)
		{
			return;
		}
		_nextCheck = realtimeSinceStartup + _config.CheckIntervalSeconds;
		if (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer() || !_config.Enabled)
		{
			if (_wasServer)
			{
				_service.Clear();
				_wasServer = false;
			}
		}
		else
		{
			_wasServer = true;
			_service.Check(realtimeSinceStartup);
		}
	}
}
internal sealed class AfkService
{
	private const int AnimatorZdoSalt = 438569;

	private readonly AFKConfig _config;

	private readonly ManualLogSource _log;

	private readonly AnnouncementService _announcer;

	private readonly Dictionary<long, PlayerAfkState> _states = new Dictionary<long, PlayerAfkState>();

	private readonly List<long> _stale = new List<long>();

	private readonly int _craftingKey;

	private int[] _equipmentKeys;

	private int _generation;

	internal AfkService(AFKConfig config, ManualLogSource log, AnnouncementService announcer)
	{
		_config = config;
		_log = log;
		_announcer = announcer;
		_craftingKey = 438569 + Animator.StringToHash("crafting");
	}

	internal bool IsAfk(long peerUid)
	{
		if (_states.TryGetValue(peerUid, out var value) && value.Initialized)
		{
			return value.IsAfk;
		}
		return false;
	}

	internal void Clear()
	{
		_states.Clear();
	}

	internal void Check(float now)
	{
		//IL_01d4: Unknown result type (might be due to invalid IL or missing references)
		ZNet instance = ZNet.instance;
		ZDOMan instance2 = ZDOMan.instance;
		if ((Object)(object)instance == (Object)null || instance2 == null)
		{
			return;
		}
		bool debugLogging = _config.DebugLogging;
		long num = (debugLogging ? Stopwatch.GetTimestamp() : 0);
		if (_equipmentKeys == null)
		{
			_equipmentKeys = new int[9]
			{
				ZDOVars.s_rightItem,
				ZDOVars.s_leftItem,
				ZDOVars.s_rightBackItem,
				ZDOVars.s_leftBackItem,
				ZDOVars.s_chestItem,
				ZDOVars.s_legItem,
				ZDOVars.s_helmetItem,
				ZDOVars.s_shoulderItem,
				ZDOVars.s_utilityItem
			};
		}
		float afkAfterSeconds = _config.AfkAfterSeconds;
		float movementThresholdMeters = _config.MovementThresholdMeters;
		float moveThresholdSqr = movementThresholdMeters * movementThresholdMeters;
		float lookThresholdDegrees = _config.LookThresholdDegrees;
		_generation++;
		int num2 = 0;
		int num3 = 0;
		List<ZNetPeer> peers = instance.GetPeers();
		for (int i = 0; i < peers.Count; i++)
		{
			ZNetPeer val = peers[i];
			if (val == null || !val.IsReady())
			{
				continue;
			}
			if (!_states.TryGetValue(val.m_uid, out var value))
			{
				value = new PlayerAfkState();
				value.PeerUid = val.m_uid;
				_states[val.m_uid] = value;
			}
			value.SeenGeneration = _generation;
			value.PlayerName = val.m_playerName;
			UpdatePlayer(value, val, instance2, now, moveThresholdSqr, lookThresholdDegrees);
			if (value.Initialized && !value.NeedsBaseline && !value.IsAfk && now - value.LastActivityTime >= afkAfterSeconds)
			{
				value.IsAfk = true;
				if (debugLogging)
				{
					_log.LogInfo((object)(value.PlayerName + " ACTIVE -> AFK"));
				}
				_announcer.AnnounceAfk(value.PlayerName, value.LastWorldPosition);
			}
			if (value.Initialized)
			{
				num2++;
				if (value.IsAfk)
				{
					num3++;
				}
			}
		}
		RemoveDisconnected();
		if (debugLogging)
		{
			double num4 = (double)(Stopwatch.GetTimestamp() - num) * 1000.0 / (double)Stopwatch.Frequency;
			_log.LogInfo((object)("check: players=" + num2 + " active=" + (num2 - num3) + " afk=" + num3 + " elapsed=" + num4.ToString("F3") + "ms"));
		}
	}

	private void UpdatePlayer(PlayerAfkState state, ZNetPeer peer, ZDOMan zdoMan, float now, float moveThresholdSqr, float lookThreshold)
	{
		//IL_0001: 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_0011: Unknown result type (might be due to invalid IL or missing references)
		//IL_0036: 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_003d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0042: Unknown result type (might be due to invalid IL or missing references)
		//IL_005e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0052: Unknown result type (might be due to invalid IL or missing references)
		//IL_0057: Unknown result type (might be due to invalid IL or missing references)
		//IL_005f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0067: Unknown result type (might be due to invalid IL or missing references)
		//IL_006c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0071: Unknown result type (might be due to invalid IL or missing references)
		//IL_0073: Unknown result type (might be due to invalid IL or missing references)
		//IL_0075: Unknown result type (might be due to invalid IL or missing references)
		//IL_008c: Unknown result type (might be due to invalid IL or missing references)
		//IL_008e: Unknown result type (might be due to invalid IL or missing references)
		//IL_008f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0094: Unknown result type (might be due to invalid IL or missing references)
		//IL_0098: Unknown result type (might be due to invalid IL or missing references)
		//IL_0085: Unknown result type (might be due to invalid IL or missing references)
		//IL_009d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0122: Unknown result type (might be due to invalid IL or missing references)
		//IL_0124: Unknown result type (might be due to invalid IL or missing references)
		//IL_0292: Unknown result type (might be due to invalid IL or missing references)
		//IL_0293: Unknown result type (might be due to invalid IL or missing references)
		//IL_0299: Unknown result type (might be due to invalid IL or missing references)
		//IL_029a: Unknown result type (might be due to invalid IL or missing references)
		//IL_02a0: Unknown result type (might be due to invalid IL or missing references)
		//IL_02a1: Unknown result type (might be due to invalid IL or missing references)
		//IL_02a7: Unknown result type (might be due to invalid IL or missing references)
		//IL_02a9: Unknown result type (might be due to invalid IL or missing references)
		//IL_02e9: Unknown result type (might be due to invalid IL or missing references)
		//IL_02eb: Unknown result type (might be due to invalid IL or missing references)
		//IL_01d4: Unknown result type (might be due to invalid IL or missing references)
		//IL_01d6: Unknown result type (might be due to invalid IL or missing references)
		//IL_0243: Unknown result type (might be due to invalid IL or missing references)
		//IL_0246: Unknown result type (might be due to invalid IL or missing references)
		ZDOID characterID = peer.m_characterID;
		ZDO val = (((ZDOID)(ref characterID)).IsNone() ? null : zdoMan.GetZDO(characterID));
		if (val == null || val.GetBool(ZDOVars.s_dead, false))
		{
			state.NeedsBaseline = true;
			return;
		}
		ZDOID connectionZDOID = val.GetConnectionZDOID((ConnectionType)2);
		Vector3 position = val.GetPosition();
		Vector3 lastPosition = (((ZDOID)(ref connectionZDOID)).IsNone() ? position : val.GetVec3(ZDOVars.s_relPosHash, Vector3.zero));
		Vector3 vec = val.GetVec3(ZDOVars.s_lookTarget, Vector3.zero);
		bool flag = vec != Vector3.zero;
		Vector3 val2;
		if (!flag)
		{
			val2 = Vector3.zero;
		}
		else
		{
			Vector3 val3 = vec - position;
			val2 = ((Vector3)(ref val3)).normalized;
		}
		Vector3 val4 = val2;
		int num = val.GetInt(ZDOVars.s_emoteID, 0);
		string text = val.GetString(ZDOVars.s_emote, "");
		int num2 = val.GetInt(_craftingKey, 0);
		bool flag2 = val.GetBool(ZDOVars.s_inBed, false);
		int num3 = 17;
		for (int i = 0; i < _equipmentKeys.Length; i++)
		{
			num3 = num3 * 31 + val.GetInt(_equipmentKeys[i], 0);
		}
		ActivityReason activityReason = ActivityReason.None;
		bool flag3 = state.Initialized && !state.NeedsBaseline && characterID == state.CharacterId;
		bool flag4 = IsManualAfkEmote(text);
		bool flag5 = flag3 && flag4 && (text != state.LastEmote || num != state.LastEmoteId);
		if (!state.Initialized)
		{
			state.Initialized = true;
			state.LastActivityTime = now;
		}
		else if (flag3)
		{
			if (flag2 && !state.WasInBed)
			{
				activityReason |= ActivityReason.EnterBed;
			}
			if (num != state.LastEmoteId && !flag4)
			{
				activityReason |= ActivityReason.Emote;
			}
			if (num3 != state.LastEquipmentHash)
			{
				activityReason |= ActivityReason.Equipment;
			}
			if (num2 != state.LastCrafting)
			{
				activityReason |= ActivityReason.CraftingStation;
			}
			if (!(connectionZDOID != state.Parent) && flag2 == state.WasInBed)
			{
				float num4 = lastPosition.x - state.LastPosition.x;
				float num5 = lastPosition.z - state.LastPosition.z;
				if (num4 * num4 + num5 * num5 > moveThresholdSqr)
				{
					activityReason |= ActivityReason.Movement;
				}
				if (flag && state.HasLookReference && Vector3.Angle(val4, state.LookReference) > lookThreshold)
				{
					activityReason |= ActivityReason.Look;
				}
			}
			else
			{
				state.HasLookReference = false;
			}
		}
		else
		{
			state.HasLookReference = false;
			state.LastActivityTime += now - state.LastObservedTime;
		}
		state.NeedsBaseline = false;
		state.LastObservedTime = now;
		state.CharacterId = characterID;
		state.Parent = connectionZDOID;
		state.LastWorldPosition = position;
		state.LastPosition = lastPosition;
		state.LastEmoteId = num;
		state.LastEmote = text;
		state.LastEquipmentHash = num3;
		state.LastCrafting = num2;
		state.WasInBed = flag2;
		if (flag && (!state.HasLookReference || (activityReason & ActivityReason.Look) != ActivityReason.None))
		{
			state.LookReference = val4;
			state.HasLookReference = true;
		}
		if (flag5)
		{
			MarkManualAfk(state, now);
		}
		else if (activityReason != ActivityReason.None)
		{
			MarkActivity(state, activityReason, now);
		}
	}

	private bool IsManualAfkEmote(string emote)
	{
		string manualAfkEmote = _config.ManualAfkEmote;
		if (emote.Length != 0 && manualAfkEmote.Length != 0)
		{
			return string.Equals(emote, manualAfkEmote, StringComparison.OrdinalIgnoreCase);
		}
		return false;
	}

	private void MarkManualAfk(PlayerAfkState state, float now)
	{
		//IL_0074: Unknown result type (might be due to invalid IL or missing references)
		state.LastActivityTime = now;
		if (state.IsAfk)
		{
			if (_config.DebugLogging)
			{
				_log.LogInfo((object)(state.PlayerName + " manual AFK emote while already AFK: ignored"));
			}
			return;
		}
		state.IsAfk = true;
		if (_config.DebugLogging)
		{
			_log.LogInfo((object)(state.PlayerName + " ACTIVE -> AFK (manual emote)"));
		}
		_announcer.AnnounceAfk(state.PlayerName, state.LastWorldPosition);
	}

	private void MarkActivity(PlayerAfkState state, ActivityReason reasons, float now)
	{
		//IL_006a: Unknown result type (might be due to invalid IL or missing references)
		state.LastActivityTime = now;
		if (state.IsAfk)
		{
			state.IsAfk = false;
			if (_config.DebugLogging)
			{
				_log.LogInfo((object)string.Concat(state.PlayerName, " AFK -> ACTIVE (", reasons, ")"));
			}
			_announcer.AnnounceReturn(state.PlayerName, state.LastWorldPosition);
		}
		else if (_config.DebugLogging)
		{
			_log.LogInfo((object)(state.PlayerName + " activity: " + reasons));
		}
	}

	private void RemoveDisconnected()
	{
		if (_states.Count == 0)
		{
			return;
		}
		_stale.Clear();
		foreach (KeyValuePair<long, PlayerAfkState> state in _states)
		{
			if (state.Value.SeenGeneration != _generation)
			{
				_stale.Add(state.Key);
			}
		}
		for (int i = 0; i < _stale.Count; i++)
		{
			if (_config.DebugLogging)
			{
				_log.LogInfo((object)("session ended: " + _states[_stale[i]].PlayerName));
			}
			_states.Remove(_stale[i]);
		}
	}
}
internal sealed class AnnouncementService
{
	private const int MaxNameLength = 32;

	private const string ChatSenderName = "AFKManager";

	private const string ChatSenderId = "AFKManager_0";

	private static readonly Vector3 ChatBubbleOffset = new Vector3(0f, 1.9f, 0f);

	private readonly AFKConfig _config;

	private readonly ManualLogSource _log;

	private readonly List<ZNetPeer> _recipients = new List<ZNetPeer>();

	private readonly MethodInfo _sendPlayerList;

	private readonly MethodInfo _writePlayerInfo;

	private bool _chatWarned;

	private bool _notificationWarned;

	internal AnnouncementService(AFKConfig config, ManualLogSource log)
	{
		_config = config;
		_log = log;
		try
		{
			MethodInfo method = typeof(ZNet).GetMethod("SendPlayerList", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
			if (method != null && method.ReturnType == typeof(void))
			{
				_sendPlayerList = method;
			}
			MethodInfo method2 = typeof(ZNet).GetMethod("WritePlayerInfo", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(List<PlayerInfo>) }, null);
			if (method2 != null && method2.ReturnType == typeof(ZPackage))
			{
				_writePlayerInfo = method2;
			}
		}
		catch (Exception ex)
		{
			_log.LogWarning((object)("Could not inspect ZNet (" + ex.GetType().Name + "): chat announcements use the built-in fallback."));
		}
		if (_sendPlayerList == null || _writePlayerInfo == null)
		{
			_log.LogWarning((object)"ZNet.SendPlayerList/WritePlayerInfo not found in this game version: chat announcements use a fallback that may hide other mods' player-list entries for up to 2 seconds per announcement.");
		}
	}

	internal void AnnounceAfk(string playerName, Vector3 playerPosition)
	{
		//IL_001a: Unknown result type (might be due to invalid IL or missing references)
		if (_config.AnnounceAfk)
		{
			Send(_config.AfkMessage, playerName, playerPosition);
		}
	}

	internal void AnnounceReturn(string playerName, Vector3 playerPosition)
	{
		//IL_001a: Unknown result type (might be due to invalid IL or missing references)
		if (_config.AnnounceReturn)
		{
			Send(_config.ReturnMessage, playerName, playerPosition);
		}
	}

	private void Send(string template, string playerName, Vector3 playerPosition)
	{
		//IL_0093: Unknown result type (might be due to invalid IL or missing references)
		//IL_0094: Unknown result type (might be due to invalid IL or missing references)
		//IL_0099: Unknown result type (might be due to invalid IL or missing references)
		if (string.IsNullOrEmpty(template) || ZRoutedRpc.instance == null)
		{
			return;
		}
		string text = template.Replace("{player}", SanitizeName(playerName));
		if (_config.AnnounceNotification)
		{
			try
			{
				ZRoutedRpc.instance.InvokeRoutedRPC(0L, "ShowMessage", new object[2] { 1, text });
			}
			catch (Exception e)
			{
				if (!_notificationWarned)
				{
					_notificationWarned = true;
					_log.LogWarning((object)("Notification announcement failed: " + Describe(e)));
				}
			}
		}
		if (_config.AnnounceInChat)
		{
			try
			{
				SendChat(text, playerPosition + ChatBubbleOffset);
			}
			catch (Exception e2)
			{
				WarnChat("Chat announcement failed (notifications are unaffected): " + Describe(e2));
			}
		}
		if (_config.DebugLogging)
		{
			_log.LogInfo((object)("announced: " + text));
		}
	}

	private void SendChat(string text, Vector3 bubblePosition)
	{
		//IL_007f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0086: Expected O, but got Unknown
		//IL_0099: Unknown result type (might be due to invalid IL or missing references)
		//IL_009e: Unknown result type (might be due to invalid IL or missing references)
		//IL_011f: Unknown result type (might be due to invalid IL or missing references)
		ZNet instance = ZNet.instance;
		if ((Object)(object)instance == (Object)null || !instance.IsServer())
		{
			return;
		}
		_recipients.Clear();
		List<ZNetPeer> peers = instance.GetPeers();
		for (int i = 0; i < peers.Count; i++)
		{
			if (peers[i] != null && peers[i].IsReady())
			{
				_recipients.Add(peers[i]);
			}
		}
		if (_recipients.Count == 0)
		{
			return;
		}
		bool environmentList;
		int entries;
		ZPackage val = BuildTemporaryList(instance, out environmentList, out entries);
		UserInfo val2 = new UserInfo();
		val2.Name = "AFKManager";
		val2.UserId = new PlatformUserID("AFKManager_0");
		bool flag = false;
		bool flag2 = false;
		int num = 0;
		try
		{
			for (int j = 0; j < _recipients.Count; j++)
			{
				ZNetPeer val3 = _recipients[j];
				try
				{
					if (val3.m_rpc != null && val3.m_rpc.IsConnected())
					{
						flag = true;
						val3.m_rpc.Invoke("PlayerList", new object[1] { val });
						ZRoutedRpc.instance.InvokeRoutedRPC(val3.m_uid, "ChatMessage", new object[4] { bubblePosition, 1, val2, text });
						num++;
					}
				}
				catch (Exception e)
				{
					WarnChat("Chat announcement to one player failed (others and notifications are unaffected): " + Describe(e));
				}
			}
		}
		finally
		{
			if (flag)
			{
				flag2 = RestorePlayerList(instance);
			}
			_recipients.Clear();
		}
		if (_config.DebugLogging)
		{
			_log.LogInfo((object)("chat: delivered=" + num + " temporaryList=" + entries + " entries (" + (environmentList ? "environment+1" : "rebuilt+1") + ") restore=" + (flag2 ? "vanilla SendPlayerList" : "fallback")));
		}
	}

	private ZPackage BuildTemporaryList(ZNet znet, out bool environmentList, out int entries)
	{
		//IL_0039: Unknown result type (might be due to invalid IL or missing references)
		//IL_003f: Expected O, but got Unknown
		//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
		//IL_00fb: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b3: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b9: Expected O, but got Unknown
		environmentList = false;
		entries = 0;
		ZPackage val = null;
		if (_writePlayerInfo != null)
		{
			try
			{
				val = (ZPackage)_writePlayerInfo.Invoke(znet, new object[1] { znet.GetPlayerList() });
				int num = val.Size();
				val.SetPos(0);
				int num2 = val.ReadInt();
				if (num2 < 0 || num < 4)
				{
					throw new InvalidOperationException("unexpected player list package");
				}
				val.SetPos(0);
				val.Write(num2 + 1);
				entries = num2 + 1;
				val.SetPos(num);
				environmentList = true;
			}
			catch (Exception e)
			{
				val = null;
				WarnChat("ZNet.WritePlayerInfo could not be used (" + Describe(e) + "); chat announcements use a rebuilt player list.");
			}
		}
		if (val == null)
		{
			List<PlayerInfo> playerList = znet.GetPlayerList();
			val = new ZPackage();
			val.Write(playerList.Count + 1);
			entries = playerList.Count + 1;
			WritePlayers(val, playerList);
		}
		WriteEntry(val, "AFKManager", ZDOID.None, "AFKManager_0", "AFKManager", "", "", publicPosition: false, Vector3.zero);
		return val;
	}

	private bool RestorePlayerList(ZNet znet)
	{
		//IL_0049: Unknown result type (might be due to invalid IL or missing references)
		//IL_004f: Expected O, but got Unknown
		if (_sendPlayerList != null)
		{
			try
			{
				_sendPlayerList.Invoke(znet, null);
				return true;
			}
			catch (Exception e)
			{
				WarnChat("ZNet.SendPlayerList failed (" + Describe(e) + "); the player list was restored by the fallback.");
			}
		}
		try
		{
			List<PlayerInfo> playerList = znet.GetPlayerList();
			ZPackage val = new ZPackage();
			val.Write(playerList.Count);
			WritePlayers(val, playerList);
			for (int i = 0; i < _recipients.Count; i++)
			{
				try
				{
					if (_recipients[i].m_rpc != null)
					{
						_recipients[i].m_rpc.Invoke("PlayerList", new object[1] { val });
					}
				}
				catch (Exception)
				{
				}
			}
		}
		catch (Exception e2)
		{
			WarnChat("Player list fallback restore failed (" + Describe(e2) + "); vanilla re-sends the list within 2 seconds.");
		}
		return false;
	}

	private unsafe static void WritePlayers(ZPackage pkg, List<PlayerInfo> players)
	{
		//IL_0006: Unknown result type (might be due to invalid IL or missing references)
		//IL_000b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0016: Unknown result type (might be due to invalid IL or missing references)
		//IL_005f: Unknown result type (might be due to invalid IL or missing references)
		for (int i = 0; i < players.Count; i++)
		{
			PlayerInfo val = players[i];
			WriteEntry(pkg, val.m_name, val.m_characterID, ((object)(*(PlatformUserID*)(&val.m_userInfo.m_id))/*cast due to .constrained prefix*/).ToString(), val.m_userInfo.m_displayName, val.m_userInfo.m_serverAssignedDisplayName, val.m_userInfo.m_playfabId, val.m_publicPosition, val.m_position);
		}
	}

	private static void WriteEntry(ZPackage pkg, string name, ZDOID characterId, string platformId, string displayName, string serverAssignedName, string playfabId, bool publicPosition, Vector3 position)
	{
		//IL_0011: Unknown result type (might be due to invalid IL or missing references)
		//IL_0067: Unknown result type (might be due to invalid IL or missing references)
		pkg.Write(name ?? "");
		pkg.Write(characterId);
		pkg.Write(platformId ?? "");
		pkg.Write(displayName ?? "");
		pkg.Write(serverAssignedName ?? "");
		pkg.Write(playfabId ?? "");
		pkg.Write(publicPosition);
		if (publicPosition)
		{
			pkg.Write(position);
		}
	}

	private void WarnChat(string message)
	{
		if (!_chatWarned)
		{
			_chatWarned = true;
			_log.LogWarning((object)message);
		}
		else if (_config.DebugLogging)
		{
			_log.LogInfo((object)message);
		}
	}

	private static string Describe(Exception e)
	{
		while (e is TargetInvocationException && e.InnerException != null)
		{
			e = e.InnerException;
		}
		return e.GetType().Name + ": " + e.Message;
	}

	internal static string SanitizeName(string name)
	{
		if (string.IsNullOrEmpty(name))
		{
			return "?";
		}
		StringBuilder stringBuilder = new StringBuilder(name.Length);
		for (int i = 0; i < name.Length; i++)
		{
			if (stringBuilder.Length >= 32)
			{
				break;
			}
			char c = name[i];
			if (c != '<' && c != '>' && c != '$' && !char.IsControl(c))
			{
				stringBuilder.Append(c);
			}
		}
		string text = stringBuilder.ToString().Trim();
		if (text.Length != 0)
		{
			return text;
		}
		return "?";
	}
}
[Flags]
internal enum ActivityReason
{
	None = 0,
	Movement = 1,
	Look = 2,
	Emote = 4,
	Equipment = 8,
	CraftingStation = 0x10,
	EnterBed = 0x20
}
internal sealed class PlayerAfkState
{
	internal long PeerUid;

	internal string PlayerName = "";

	internal bool Initialized;

	internal bool NeedsBaseline;

	internal float LastActivityTime;

	internal float LastObservedTime;

	internal bool IsAfk;

	internal ZDOID CharacterId;

	internal ZDOID Parent;

	internal Vector3 LastPosition;

	internal Vector3 LastWorldPosition;

	internal Vector3 LookReference;

	internal bool HasLookReference;

	internal int LastEmoteId;

	internal string LastEmote = "";

	internal int LastEquipmentHash;

	internal int LastCrafting;

	internal bool WasInBed;

	internal int SeenGeneration;
}
internal static class RaidIntegration
{
	private const string HarmonyId = "torokal.afkmanager.raids";

	private static AfkService _service;

	private static AFKConfig _config;

	private static ManualLogSource _log;

	private static bool _errorLogged;

	private static readonly List<Vector3> _playerPositions = new List<Vector3>();

	private static readonly List<bool> _playerAfk = new List<bool>();

	internal static void TryEnable(AfkService service, AFKConfig config, ManualLogSource log)
	{
		//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c5: Expected O, but got Unknown
		_service = service;
		_config = config;
		_log = log;
		if (!config.ExcludeAfkFromRandomRaids)
		{
			log.LogInfo((object)"Raid integration disabled by config.");
			return;
		}
		try
		{
			MethodInfo methodInfo = AccessTools.Method(typeof(RandEventSystem), "GetValidEventPoints", (Type[])null, (Type[])null);
			ParameterInfo[] array = ((methodInfo != null) ? methodInfo.GetParameters() : null);
			if (methodInfo == null || methodInfo.ReturnType != typeof(List<Vector3>) || array.Length < 1 || array[0].ParameterType != typeof(RandomEvent))
			{
				log.LogWarning((object)"Raid integration unavailable: RandEventSystem.GetValidEventPoints(RandomEvent, ...) not found in this game version. Everything else is unaffected.");
				return;
			}
			new Harmony("torokal.afkmanager.raids").Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(RaidIntegration), "Postfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			log.LogInfo((object)"Raid integration enabled (Postfix on RandEventSystem.GetValidEventPoints).");
		}
		catch (Exception ex)
		{
			log.LogWarning((object)("Raid integration could not be enabled (" + ex.GetType().Name + ": " + ex.Message + "). Everything else is unaffected."));
		}
	}

	private static void Postfix(RandomEvent __0, List<Vector3> __result)
	{
		if (__result == null || __result.Count == 0 || __0 == null || _config == null || !_config.Enabled || !_config.ExcludeAfkFromRandomRaids)
		{
			return;
		}
		try
		{
			int num = RemoveAfkOnlyAreas(__0, __result);
			if (num > 0 && _config.DebugLogging)
			{
				_log.LogInfo((object)("Random raid '" + __0.m_name + "': " + num + " AFK-only target area(s) not eligible, " + __result.Count + " left."));
			}
		}
		catch (Exception ex)
		{
			if (!_errorLogged)
			{
				_errorLogged = true;
				_log.LogError((object)("Raid integration error (vanilla result kept): " + ex));
			}
		}
	}

	private static int RemoveAfkOnlyAreas(RandomEvent randomEvent, List<Vector3> points)
	{
		//IL_0058: Unknown result type (might be due to invalid IL or missing references)
		ZNet instance = ZNet.instance;
		if ((Object)(object)instance == (Object)null || _service == null || !instance.IsServer())
		{
			return 0;
		}
		_playerPositions.Clear();
		_playerAfk.Clear();
		List<ZNetPeer> peers = instance.GetPeers();
		for (int i = 0; i < peers.Count; i++)
		{
			ZNetPeer val = peers[i];
			if (val != null && val.IsReady())
			{
				_playerPositions.Add(val.m_refPos);
				_playerAfk.Add(_service.IsAfk(val.m_uid));
			}
		}
		return RemoveAfkOnlyAreas(points, randomEvent.m_eventRange, _playerPositions, _playerAfk);
	}

	internal static int RemoveAfkOnlyAreas(List<Vector3> points, float eventRange, List<Vector3> playerPositions, List<bool> playerAfk)
	{
		//IL_0022: Unknown result type (might be due to invalid IL or missing references)
		//IL_0027: Unknown result type (might be due to invalid IL or missing references)
		//IL_0037: 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)
		float num = Mathf.Max(eventRange, 1f);
		float num2 = num * num;
		int num3 = 0;
		for (int num4 = points.Count - 1; num4 >= 0; num4--)
		{
			Vector3 val = points[num4];
			bool flag = false;
			bool flag2 = false;
			for (int i = 0; i < playerPositions.Count; i++)
			{
				float num5 = playerPositions[i].x - val.x;
				float num6 = playerPositions[i].z - val.z;
				if (!(num5 * num5 + num6 * num6 >= num2))
				{
					flag = true;
					if (!playerAfk[i])
					{
						flag2 = true;
						break;
					}
				}
			}
			if (flag && !flag2)
			{
				points.RemoveAt(num4);
				num3++;
			}
		}
		return num3;
	}
}
internal static class SleepIntegration
{
	private const string HarmonyId = "torokal.afkmanager.sleep";

	private static AfkService _service;

	private static AFKConfig _config;

	private static ManualLogSource _log;

	private static bool _errorLogged;

	internal static void TryEnable(AfkService service, AFKConfig config, ManualLogSource log)
	{
		//IL_0072: Unknown result type (might be due to invalid IL or missing references)
		//IL_0089: Unknown result type (might be due to invalid IL or missing references)
		//IL_0096: Expected O, but got Unknown
		_service = service;
		_config = config;
		_log = log;
		if (!config.ExcludeAfkFromSleep)
		{
			log.LogInfo((object)"Sleep integration disabled by config.");
			return;
		}
		try
		{
			MethodInfo methodInfo = AccessTools.Method(typeof(Game), "EverybodyIsTryingToSleep", (Type[])null, (Type[])null);
			if (methodInfo == null || methodInfo.ReturnType != typeof(bool))
			{
				log.LogWarning((object)"Sleep integration unavailable: Game.EverybodyIsTryingToSleep() not found in this game version. AFK detection is unaffected.");
				return;
			}
			new Harmony("torokal.afkmanager.sleep").Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(SleepIntegration), "Postfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			log.LogInfo((object)"Sleep integration enabled (Postfix on Game.EverybodyIsTryingToSleep).");
			MethodInfo methodInfo2 = AccessTools.Method(typeof(Game), "UpdateSleeping", (Type[])null, (Type[])null);
			Patches val = ((methodInfo2 != null) ? Harmony.GetPatchInfo((MethodBase)methodInfo2) : null);
			if (val != null && val.Prefixes.Count > 0)
			{
				log.LogWarning((object)"Another mod patches Game.UpdateSleeping; if it replaces the vanilla sleep loop, ExcludeAfkFromSleep may have no effect.");
			}
		}
		catch (Exception ex)
		{
			log.LogWarning((object)("Sleep integration could not be enabled (" + ex.GetType().Name + ": " + ex.Message + "). AFK detection is unaffected."));
		}
	}

	private static void Postfix(ref bool __result)
	{
		if (__result || _config == null || !_config.Enabled || !_config.ExcludeAfkFromSleep)
		{
			return;
		}
		try
		{
			if (Evaluate())
			{
				__result = true;
			}
		}
		catch (Exception ex)
		{
			if (!_errorLogged)
			{
				_errorLogged = true;
				_log.LogError((object)("Sleep integration error (vanilla result kept): " + ex));
			}
		}
	}

	private static bool Evaluate()
	{
		//IL_005f: Unknown result type (might be due to invalid IL or missing references)
		ZNet instance = ZNet.instance;
		ZDOMan instance2 = ZDOMan.instance;
		if ((Object)(object)instance == (Object)null || instance2 == null || _service == null || !instance.IsServer())
		{
			return false;
		}
		bool result = false;
		List<ZNetPeer> peers = instance.GetPeers();
		for (int i = 0; i < peers.Count; i++)
		{
			ZNetPeer val = peers[i];
			if (val == null || !val.IsReady() || ((ZDOID)(ref val.m_characterID)).IsNone())
			{
				continue;
			}
			ZDO zDO = instance2.GetZDO(val.m_characterID);
			if (zDO != null)
			{
				if (zDO.GetBool(ZDOVars.s_inBed, false))
				{
					result = true;
				}
				else if (!_service.IsAfk(val.m_uid))
				{
					return false;
				}
			}
		}
		return result;
	}
}