Decompiled source of Whitelist Remove v1.0.2

plugins/WhitelistRemove.dll

Decompiled a month ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: AssemblyFileVersion("1.0.2")]
[assembly: Guid("E74EB49A-461D-48EA-85BC-F462D60C98C4")]
[assembly: ComVisible(false)]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCopyright("Copyright ©  2025")]
[assembly: AssemblyProduct("WhitelistRemove")]
[assembly: AssemblyCompany("Radamanto")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyTitle("WhitelistRemove")]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: CompilationRelaxations(8)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.2.0")]
[module: UnverifiableCode]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	[Embedded]
	[CompilerGenerated]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	[Embedded]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
}
namespace WhitelistRemove
{
	[BepInPlugin("radamanto.WhitelistRemove", "WhitelistRemove", "1.0.2")]
	public sealed class WhitelistRemovePlugin : BaseUnityPlugin
	{
		private readonly struct ParsedLine
		{
			public readonly string Raw;

			public readonly bool IsCommentOrEmpty;

			public readonly string Token;

			public readonly bool IsNumericToken;

			public readonly ulong SteamId;

			public ParsedLine(string raw, bool isCommentOrEmpty, string token, bool isNumericToken, ulong steamId)
			{
				Raw = raw;
				IsCommentOrEmpty = isCommentOrEmpty;
				Token = token;
				IsNumericToken = isNumericToken;
				SteamId = steamId;
			}
		}

		[HarmonyPatch(typeof(ZNet), "Awake")]
		private static class ZNet_Awake_BeginSession
		{
			private static void Postfix(ZNet __instance)
			{
				Instance?.Guard(delegate
				{
					Instance.BeginSession(__instance);
				});
			}
		}

		[HarmonyPatch(typeof(ZNet), "OnDestroy")]
		private static class ZNet_OnDestroy_EndSession
		{
			private static void Prefix()
			{
				Instance?.Guard(Instance.EndSession);
			}
		}

		[HarmonyPatch(typeof(ZNet), "RPC_CharacterID")]
		private static class ZNet_RPC_CharacterID_RecordEntry
		{
			private static void Postfix(ZNet __instance, ZRpc rpc, ZDOID characterID)
			{
				if (!__instance.IsServer() || ((ZDOID)(ref characterID)).IsNone())
				{
					return;
				}
				ZNetPeer peer = __instance.GetPeer(rpc);
				if (peer != null)
				{
					Instance?.Guard(delegate
					{
						Instance.RecordEntry(peer);
					});
				}
			}
		}

		[HarmonyPatch(typeof(ZNet), "Disconnect")]
		private static class ZNet_Disconnect_RecordDeparture
		{
			private static void Prefix(ZNet __instance, ZNetPeer peer)
			{
				if (__instance.IsServer() && peer != null)
				{
					Instance?.Guard(delegate
					{
						Instance.RecordDeparture(peer);
					});
				}
			}
		}

		private string _bypassRawLast = string.Empty;

		private readonly HashSet<ulong> _bypassIds = new HashSet<ulong>();

		private ZNet? _session;

		private Coroutine? _scanRoutine;

		private readonly HashSet<long> _enteredPeers = new HashSet<long>();

		private ConfigEntry<bool> CE_Enable = null;

		private ConfigEntry<string> CE_WhitelistFileName = null;

		private ConfigEntry<float> CE_ScanIntervalSeconds = null;

		private ConfigEntry<float> CE_InactiveThresholdSeconds = null;

		private ConfigEntry<string> CE_BypassSteamIds = null;

		private ConfigEntry<bool> CE_BypassAdmins = null;

		internal const string ModName = "WhitelistRemove";

		internal const string ModVersion = "1.0.2";

		internal const string Author = "radamanto";

		internal const string ModGUID = "radamanto.WhitelistRemove";

		internal static WhitelistRemovePlugin? Instance;

		private readonly Harmony _harmony = new Harmony("radamanto.WhitelistRemove");

		private string _lastSeenPath = string.Empty;

		private string _removedLogPath = string.Empty;

		private void RefreshBypassCacheIfNeeded()
		{
			string text = CE_BypassSteamIds.Value ?? string.Empty;
			if (string.Equals(text, _bypassRawLast, StringComparison.Ordinal))
			{
				return;
			}
			_bypassRawLast = text;
			_bypassIds.Clear();
			string[] array = text.Split(new char[5] { ',', ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i].Trim();
				if (text2.Length != 0 && !text2.StartsWith("//", StringComparison.Ordinal) && !text2.StartsWith("#", StringComparison.Ordinal) && !text2.StartsWith(";", StringComparison.Ordinal) && PlatformId.TryGetSteamNumber(text2, out var steamId))
				{
					_bypassIds.Add(steamId);
				}
			}
		}

		private bool IsBypassed(ulong steamId)
		{
			if (_bypassIds.Contains(steamId))
			{
				return true;
			}
			if (CE_BypassAdmins.Value)
			{
				try
				{
					if ((Object)(object)_session != (Object)null && (_session.IsAdmin(PlatformId.ToVanillaToken(steamId)) || _session.IsAdmin(steamId.ToString(CultureInfo.InvariantCulture))))
					{
						return true;
					}
				}
				catch (Exception innerException)
				{
					throw new InvalidOperationException("Could not verify administrator bypass.", innerException);
				}
			}
			return false;
		}

		private void AppendRemovedLog(string removedLogPath, ulong steamId, long lastSeen, long inactiveSeconds, string reason)
		{
			try
			{
				Directory.CreateDirectory(Path.GetDirectoryName(removedLogPath) ?? ".");
				string text = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss 'UTC'", CultureInfo.InvariantCulture);
				string text2 = DateTimeOffset.FromUnixTimeSeconds(lastSeen).ToString("yyyy-MM-dd HH:mm:ss 'UTC'", CultureInfo.InvariantCulture);
				string text3 = "[" + text + "] removed steamid=" + steamId.ToString(CultureInfo.InvariantCulture) + " reason=" + reason + " lastSeen=" + text2 + " inactiveSeconds=" + inactiveSeconds.ToString(CultureInfo.InvariantCulture);
				File.AppendAllText(removedLogPath, text3 + Environment.NewLine, Encoding.UTF8);
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Removal log could not be written: " + ex.Message));
			}
		}

		private static string FirstToken(string trimmed)
		{
			int i;
			for (i = 0; i < trimmed.Length && !char.IsWhiteSpace(trimmed[i]); i++)
			{
			}
			return (i <= 0) ? string.Empty : trimmed.Substring(0, i);
		}

		private IEnumerator RunScanOnceCoroutine()
		{
			if ((Object)(object)_session == (Object)null || !_session.IsServer() || !CE_Enable.Value || (!SeenStore.Ready && !SeenStore.Load(_lastSeenPath, ((BaseUnityPlugin)this).Logger)))
			{
				yield break;
			}
			RefreshBypassCacheIfNeeded();
			string fileName = (CE_WhitelistFileName.Value ?? "permittedlist.txt").Trim();
			if (string.IsNullOrWhiteSpace(fileName))
			{
				yield break;
			}
			string root = Utils.GetSaveDataPath((FileSource)2);
			if (string.IsNullOrWhiteSpace(root))
			{
				yield break;
			}
			string whitelistPath = Path.Combine(root, fileName);
			if (!File.Exists(whitelistPath))
			{
				yield break;
			}
			long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
			float configuredThreshold = CE_InactiveThresholdSeconds.Value;
			if (float.IsNaN(configuredThreshold) || float.IsInfinity(configuredThreshold))
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Invalid inactivity threshold; cleanup suspended.");
				yield break;
			}
			long threshold = (long)Mathf.Max(1f, configuredThreshold);
			HashSet<ulong> online = OnlineSnapshot.GetOnlineSteamIds(_session);
			bool onlineSeenChanged = false;
			foreach (ulong osid in online)
			{
				SeenStore.SetLastSeen(osid, now);
				onlineSeenChanged = true;
			}
			string[] lines = Array.Empty<string>();
			bool readOk = false;
			int readAttempt = 0;
			while (readAttempt < 3)
			{
				bool delay;
				try
				{
					lines = File.ReadAllLines(whitelistPath, Encoding.UTF8);
					readOk = true;
				}
				catch (IOException ex)
				{
					IOException error = ex;
					readAttempt++;
					if (readAttempt >= 3)
					{
						((BaseUnityPlugin)this).Logger.LogWarning((object)("Whitelist could not be read: " + error.Message));
						break;
					}
					delay = true;
					goto IL_0313;
				}
				catch (Exception ex2)
				{
					Exception error2 = ex2;
					((BaseUnityPlugin)this).Logger.LogWarning((object)("Whitelist file operation failed: " + error2.Message));
				}
				break;
				IL_0313:
				if (delay)
				{
					yield return (object)new WaitForSecondsRealtime(0.2f);
				}
			}
			if (!readOk)
			{
				yield break;
			}
			List<ParsedLine> parsed = new List<ParsedLine>(lines.Length);
			for (int i = 0; i < lines.Length; i++)
			{
				string raw = lines[i] ?? string.Empty;
				string t = raw.Trim();
				if (t.Length == 0 || t.StartsWith("//", StringComparison.Ordinal) || t.StartsWith("#", StringComparison.Ordinal) || t.StartsWith(";", StringComparison.Ordinal))
				{
					parsed.Add(new ParsedLine(raw, isCommentOrEmpty: true, string.Empty, isNumericToken: false, 0uL));
					continue;
				}
				string tok = FirstToken(t);
				ulong sid;
				if (tok.Length == 0)
				{
					parsed.Add(new ParsedLine(raw, isCommentOrEmpty: true, string.Empty, isNumericToken: false, 0uL));
				}
				else if (PlatformId.TryGetSteamNumber(tok, out sid))
				{
					parsed.Add(new ParsedLine(raw, isCommentOrEmpty: false, tok, isNumericToken: true, sid));
				}
				else
				{
					parsed.Add(new ParsedLine(raw, isCommentOrEmpty: false, tok, isNumericToken: false, 0uL));
				}
			}
			if (parsed.Count == 0)
			{
				yield break;
			}
			bool lastSeenChanged = onlineSeenChanged;
			int removedCount = 0;
			Dictionary<ulong, long> pendingRemoval = new Dictionary<ulong, long>();
			HashSet<string> keptIds = new HashSet<string>(StringComparer.Ordinal);
			HashSet<ulong> keptUlong = new HashSet<ulong>();
			for (int j = 0; j < parsed.Count; j++)
			{
				ParsedLine pl = parsed[j];
				if (pl.IsCommentOrEmpty || !pl.IsNumericToken)
				{
					continue;
				}
				ulong sid2 = pl.SteamId;
				if (online.Contains(sid2))
				{
					keptIds.Add(pl.Token);
					keptUlong.Add(sid2);
					continue;
				}
				if (IsBypassed(sid2))
				{
					keptIds.Add(pl.Token);
					keptUlong.Add(sid2);
					continue;
				}
				if (!SeenStore.TryGetLastSeen(sid2, out var lastSeen))
				{
					SeenStore.SetLastSeen(sid2, now);
					lastSeenChanged = true;
					keptIds.Add(pl.Token);
					keptUlong.Add(sid2);
					continue;
				}
				long delta = now - lastSeen;
				if (delta > threshold)
				{
					removedCount++;
					pendingRemoval[sid2] = lastSeen;
				}
				else
				{
					keptIds.Add(pl.Token);
					keptUlong.Add(sid2);
				}
			}
			if ((lastSeenChanged && !SeenStore.Save(_lastSeenPath, ((BaseUnityPlugin)this).Logger)) || removedCount == 0)
			{
				yield break;
			}
			List<string> rebuilt = new List<string>(parsed.Count);
			for (int k = 0; k < parsed.Count; k++)
			{
				ParsedLine pl2 = parsed[k];
				if (pl2.IsCommentOrEmpty)
				{
					rebuilt.Add(pl2.Raw);
				}
				else if (!pl2.IsNumericToken)
				{
					rebuilt.Add(pl2.Raw);
				}
				else if (keptIds.Contains(pl2.Token))
				{
					rebuilt.Add(pl2.Raw);
				}
			}
			bool writeOk = false;
			int writeAttempt = 0;
			while (writeAttempt < 3)
			{
				bool delay2;
				try
				{
					if (!((Object)(object)_session == (Object)null) && CE_Enable.Value)
					{
						if (!File.ReadAllLines(whitelistPath, Encoding.UTF8).SequenceEqual(lines))
						{
							((BaseUnityPlugin)this).Logger.LogInfo((object)"Whitelist changed during the scan; removal postponed.");
							break;
						}
						if (Enumerable.Any(predicate: OnlineSnapshot.GetOnlineSteamIds(_session).Contains, source: pendingRemoval.Keys))
						{
							((BaseUnityPlugin)this).Logger.LogInfo((object)"A removal candidate connected during the scan; removal postponed.");
							break;
						}
						if (pendingRemoval.Any((KeyValuePair<ulong, long> entry) => SeenStore.TryGetLastSeen(entry.Key, out var lastSeen2) && lastSeen2 != entry.Value))
						{
							((BaseUnityPlugin)this).Logger.LogInfo((object)"Player activity changed during the scan; removal postponed.");
							break;
						}
						FileIoRetry.AtomicWriteText(whitelistPath, string.Join(Environment.NewLine, rebuilt) + Environment.NewLine);
						writeOk = true;
						break;
					}
					yield break;
				}
				catch (IOException ex)
				{
					IOException error3 = ex;
					writeAttempt++;
					if (writeAttempt >= 3)
					{
						((BaseUnityPlugin)this).Logger.LogWarning((object)("Whitelist could not be written: " + error3.Message));
						break;
					}
					delay2 = true;
					goto IL_0aab;
				}
				catch (Exception ex2)
				{
					Exception error4 = ex2;
					((BaseUnityPlugin)this).Logger.LogWarning((object)("Whitelist file operation failed: " + error4.Message));
				}
				break;
				IL_0aab:
				if (delay2)
				{
					yield return (object)new WaitForSecondsRealtime(0.2f);
				}
			}
			if (!writeOk)
			{
				yield break;
			}
			((BaseUnityPlugin)this).Logger.LogInfo((object)("Removed " + pendingRemoval.Count + " inactive whitelist entries."));
			if (!rebuilt.Any((string line) => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("//", StringComparison.Ordinal)))
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Whitelist is empty after cleanup; vanilla access restrictions are now disabled.");
			}
			foreach (KeyValuePair<ulong, long> removed in pendingRemoval)
			{
				SeenStore.Remove(removed.Key);
				AppendRemovedLog(_removedLogPath, removed.Key, removed.Value, now - removed.Value, "inactive");
			}
			SeenStore.PruneTo(keptUlong);
			SeenStore.Save(_lastSeenPath, ((BaseUnityPlugin)this).Logger);
		}

		internal void Guard(Action action)
		{
			try
			{
				action();
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Whitelist operation failed: " + ex.Message));
			}
		}

		internal void BeginSession(ZNet network)
		{
			EndSession();
			if (network.IsServer())
			{
				_session = network;
				SeenStore.Load(_lastSeenPath, ((BaseUnityPlugin)this).Logger);
				_scanRoutine = ((MonoBehaviour)this).StartCoroutine(ScanLoop());
			}
		}

		internal void EndSession()
		{
			if (_scanRoutine != null)
			{
				((MonoBehaviour)this).StopCoroutine(_scanRoutine);
			}
			_scanRoutine = null;
			try
			{
				if (!((Object)(object)_session != (Object)null) || !SeenStore.Ready)
				{
					return;
				}
				long unixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
				foreach (ZNetPeer peer in _session.GetPeers())
				{
					if (_enteredPeers.Contains(peer.m_uid) && PlatformId.TryGetSteamId(peer, out var steamId))
					{
						SeenStore.SetLastSeen(steamId, unixSeconds);
					}
				}
				SeenStore.Save(_lastSeenPath, ((BaseUnityPlugin)this).Logger);
			}
			finally
			{
				_session = null;
				_enteredPeers.Clear();
			}
		}

		internal void RecordEntry(ZNetPeer peer)
		{
			if (!((Object)(object)_session == (Object)null) && peer.m_uid != 0L && !((ZDOID)(ref peer.m_characterID)).IsNone() && PlatformId.TryGetSteamId(peer, out var steamId) && _enteredPeers.Add(peer.m_uid))
			{
				RecordActivity(steamId);
			}
		}

		internal void RecordDeparture(ZNetPeer peer)
		{
			if (_enteredPeers.Remove(peer.m_uid) && PlatformId.TryGetSteamId(peer, out var steamId))
			{
				RecordActivity(steamId);
			}
		}

		private void RecordActivity(ulong steamId)
		{
			if (SeenStore.Ready || SeenStore.Load(_lastSeenPath, ((BaseUnityPlugin)this).Logger))
			{
				SeenStore.SetLastSeen(steamId, DateTimeOffset.UtcNow.ToUnixTimeSeconds());
				SeenStore.Save(_lastSeenPath, ((BaseUnityPlugin)this).Logger);
			}
		}

		private IEnumerator ScanLoop()
		{
			while ((Object)(object)_session != (Object)null)
			{
				IEnumerator scan = RunScanOnceCoroutine();
				while (true)
				{
					bool moved;
					try
					{
						moved = scan.MoveNext();
					}
					catch (Exception ex)
					{
						Exception error = ex;
						((BaseUnityPlugin)this).Logger.LogWarning((object)("Whitelist scan aborted: " + error.Message));
						break;
					}
					if (!moved)
					{
						break;
					}
					yield return scan.Current;
				}
				float interval = CE_ScanIntervalSeconds.Value;
				if (float.IsNaN(interval) || float.IsInfinity(interval))
				{
					interval = 900f;
				}
				yield return (object)new WaitForSecondsRealtime(Mathf.Max(5f, interval));
			}
		}

		private void Configure()
		{
			CE_Enable = ((BaseUnityPlugin)this).Config.Bind<bool>("01 - General", "Enable", true, "Enable the whitelist inactivity cleaner.");
			CE_WhitelistFileName = ((BaseUnityPlugin)this).Config.Bind<string>("02 - Whitelist", "WhitelistFileName", "permittedlist.txt", "Whitelist file name.");
			CE_ScanIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("03 - Scan", "ScanIntervalSeconds", 900f, "How often the mod scans the whitelist and removes inactive users.");
			CE_InactiveThresholdSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("03 - Scan", "InactiveThresholdSeconds", 259200f, "If a whitelisted SteamID did not log in for more than this amount of seconds, it will be removed.");
			CE_BypassSteamIds = ((BaseUnityPlugin)this).Config.Bind<string>("04 - Bypass", "BypassSteamIds", "", "SteamIDs that are NEVER removed (comma separated).");
			CE_BypassAdmins = ((BaseUnityPlugin)this).Config.Bind<bool>("04 - Bypass", "BypassAdmins", true, "If true, SteamIDs that are admins on the server are NEVER removed.");
		}

		private void Awake()
		{
			Instance = this;
			bool saveOnConfigSet = ((BaseUnityPlugin)this).Config.SaveOnConfigSet;
			((BaseUnityPlugin)this).Config.SaveOnConfigSet = false;
			try
			{
				Configure();
			}
			finally
			{
				((BaseUnityPlugin)this).Config.SaveOnConfigSet = saveOnConfigSet;
			}
			((BaseUnityPlugin)this).Config.Save();
			string path = Path.Combine(Paths.ConfigPath, "WhitelistRemove");
			_lastSeenPath = Path.Combine(path, "lastseen.json");
			_removedLogPath = Path.Combine(path, "removed.log");
			_harmony.PatchAll(typeof(WhitelistRemovePlugin).Assembly);
		}

		private void OnDestroy()
		{
			Guard(EndSession);
			_harmony.UnpatchSelf();
			Instance = null;
		}
	}
	internal static class OnlineSnapshot
	{
		internal static HashSet<ulong> GetOnlineSteamIds(ZNet network)
		{
			HashSet<ulong> hashSet = new HashSet<ulong>();
			try
			{
				List<ZNetPeer> peers = network.GetPeers();
				for (int i = 0; i < peers.Count; i++)
				{
					ZNetPeer val = peers[i];
					if (val != null && val.m_uid != 0 && PlatformId.TryGetSteamId(val, out var steamId))
					{
						hashSet.Add(steamId);
					}
				}
			}
			catch (Exception innerException)
			{
				throw new InvalidOperationException("Could not identify connected players.", innerException);
			}
			return hashSet;
		}
	}
	internal static class PlatformId
	{
		internal static bool TryGetSteamNumber(string? value, out ulong steamId)
		{
			steamId = 0uL;
			if (string.IsNullOrWhiteSpace(value))
			{
				return false;
			}
			string text = value.Trim();
			if (text.StartsWith("V_", StringComparison.OrdinalIgnoreCase))
			{
				text = text.Substring(2);
			}
			if (text.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase))
			{
				text = text.Substring(6);
			}
			return ulong.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out steamId) && steamId != 0;
		}

		internal static string ToVanillaToken(ulong steamId)
		{
			return "V_" + steamId.ToString(CultureInfo.InvariantCulture);
		}

		internal static bool TryGetSteamId(ZNetPeer peer, out ulong steamId)
		{
			steamId = 0uL;
			if (peer?.m_socket == null)
			{
				return false;
			}
			string text = peer.m_socket.GetHostName()?.Trim() ?? "";
			int num = text.IndexOf(':');
			if (num >= 0)
			{
				text = text.Substring(0, num).Trim();
			}
			return TryGetSteamNumber(text, out steamId);
		}
	}
	internal static class FileIoRetry
	{
		internal const int MaxRetries = 3;

		internal const float RetryDelaySeconds = 0.2f;

		internal static void AtomicWriteText(string path, string content)
		{
			string path2 = Path.GetDirectoryName(path) ?? ".";
			Directory.CreateDirectory(path2);
			string text = path + ".tmp";
			File.WriteAllText(text, content, Encoding.UTF8);
			if (File.Exists(path))
			{
				File.Replace(text, path, path + ".bak", ignoreMetadataErrors: true);
			}
			else
			{
				File.Move(text, path);
			}
		}
	}
	internal static class SeenStore
	{
		private static readonly object LockObj = new object();

		private static readonly Dictionary<ulong, long> LastSeenUnixBySteamId = new Dictionary<ulong, long>();

		internal static bool Ready { get; private set; }

		internal static bool Load(string path, ManualLogSource logger)
		{
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_004b: Expected O, but got Unknown
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_004d: Expected O, but got Unknown
			lock (LockObj)
			{
				Ready = false;
				try
				{
					Dictionary<ulong, long> dictionary = new Dictionary<ulong, long>();
					if (File.Exists(path))
					{
						DataContractJsonSerializer val = new DataContractJsonSerializer(typeof(Dictionary<string, long>), new DataContractJsonSerializerSettings
						{
							UseSimpleDictionaryFormat = true
						});
						using MemoryStream memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(File.ReadAllText(path, Encoding.UTF8)));
						if (!(((XmlObjectSerializer)val).ReadObject((Stream)memoryStream) is Dictionary<string, long> dictionary2))
						{
							throw new InvalidDataException("Invalid last-seen history.");
						}
						foreach (KeyValuePair<string, long> item in dictionary2)
						{
							if (!ulong.TryParse(item.Key, NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result == 0L || item.Value < 0 || item.Value > 253402300799L)
							{
								throw new InvalidDataException("Invalid last-seen entry.");
							}
							dictionary.Add(result, item.Value);
						}
					}
					LastSeenUnixBySteamId.Clear();
					foreach (KeyValuePair<ulong, long> item2 in dictionary)
					{
						LastSeenUnixBySteamId.Add(item2.Key, item2.Value);
					}
					Ready = true;
					return true;
				}
				catch (Exception ex)
				{
					logger.LogWarning((object)("Last-seen history could not be loaded; cleanup suspended: " + ex.Message));
					return false;
				}
			}
		}

		internal static bool TryGetLastSeen(ulong steamId, out long lastSeen)
		{
			lock (LockObj)
			{
				return LastSeenUnixBySteamId.TryGetValue(steamId, out lastSeen);
			}
		}

		internal static void SetLastSeen(ulong steamId, long unixSeconds)
		{
			lock (LockObj)
			{
				LastSeenUnixBySteamId[steamId] = unixSeconds;
			}
		}

		internal static bool Remove(ulong steamId)
		{
			lock (LockObj)
			{
				return LastSeenUnixBySteamId.Remove(steamId);
			}
		}

		internal static int PruneTo(HashSet<ulong> keepSteamIds)
		{
			lock (LockObj)
			{
				if (LastSeenUnixBySteamId.Count == 0)
				{
					return 0;
				}
				int num = 0;
				ulong[] array = LastSeenUnixBySteamId.Keys.ToArray();
				foreach (ulong num2 in array)
				{
					if (!keepSteamIds.Contains(num2) && LastSeenUnixBySteamId.Remove(num2))
					{
						num++;
					}
				}
				return num;
			}
		}

		internal static bool Save(string path, ManualLogSource logger)
		{
			lock (LockObj)
			{
				if (!Ready)
				{
					return false;
				}
				try
				{
					StringBuilder stringBuilder = new StringBuilder(4096);
					stringBuilder.AppendLine("{");
					bool flag = true;
					foreach (KeyValuePair<ulong, long> item in LastSeenUnixBySteamId.OrderBy((KeyValuePair<ulong, long> k) => k.Key))
					{
						if (!flag)
						{
							stringBuilder.AppendLine(",");
						}
						flag = false;
						stringBuilder.Append("  \"");
						stringBuilder.Append(item.Key.ToString(CultureInfo.InvariantCulture));
						stringBuilder.Append("\": ");
						stringBuilder.Append(item.Value.ToString(CultureInfo.InvariantCulture));
					}
					stringBuilder.AppendLine();
					stringBuilder.AppendLine("}");
					string content = stringBuilder.ToString();
					FileIoRetry.AtomicWriteText(path, content);
					return true;
				}
				catch (Exception ex)
				{
					logger.LogWarning((object)("Last-seen history could not be saved: " + ex.Message));
					return false;
				}
			}
		}
	}
}