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 Whitelist Remove v1.0.2
plugins/WhitelistRemove.dll
Decompiled a month agousing 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; } } } } }