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.1
plugins/WhitelistRemove.dll
Decompiled 5 days 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.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.1")] [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.IgnoreSymbolStoreSequencePoints)] [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.1.0")] [module: UnverifiableCode] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] 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] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] 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.1")] public sealed class WhitelistRemovePlugin : BaseUnityPlugin { private readonly struct ParsedLine { public readonly string Raw; public readonly string Trimmed; public readonly bool IsCommentOrEmpty; public readonly string Token; public readonly bool IsNumericToken; public readonly ulong SteamId; public ParsedLine(string raw, string trimmed, bool isCommentOrEmpty, string token, bool isNumericToken, ulong steamId) { Raw = raw; Trimmed = trimmed; IsCommentOrEmpty = isCommentOrEmpty; Token = token; IsNumericToken = isNumericToken; SteamId = steamId; } } internal const string ModName = "WhitelistRemove"; internal const string ModVersion = "1.0.1"; internal const string Author = "radamanto"; internal const string ModGUID = "radamanto.WhitelistRemove"; private readonly Harmony _harmony = new Harmony("radamanto.WhitelistRemove"); private ConfigEntry<bool> CE_Enable; private ConfigEntry<string> CE_WhitelistFileName; private ConfigEntry<float> CE_ScanIntervalSeconds; private ConfigEntry<float> CE_InactiveThresholdSeconds; private ConfigEntry<string> CE_BypassSteamIds; private ConfigEntry<bool> CE_BypassAdmins; private string _dataDir = string.Empty; private string _lastSeenPath = string.Empty; private string _removedLogPath = string.Empty; private string _bypassRawLast = string.Empty; private readonly HashSet<ulong> _bypassIds = new HashSet<ulong>(); private void Awake() { bool saveOnConfigSet = ((BaseUnityPlugin)this).Config.SaveOnConfigSet; ((BaseUnityPlugin)this).Config.SaveOnConfigSet = false; try { 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."); } finally { ((BaseUnityPlugin)this).Config.SaveOnConfigSet = saveOnConfigSet; } ((BaseUnityPlugin)this).Config.Save(); _dataDir = Path.Combine(Paths.ConfigPath, "WhitelistRemove"); _lastSeenPath = Path.Combine(_dataDir, "lastseen.json"); _removedLogPath = Path.Combine(_dataDir, "removed.log"); Directory.CreateDirectory(_dataDir); SeenStore.Load(_lastSeenPath, ((BaseUnityPlugin)this).Logger); _harmony.PatchAll(typeof(WhitelistRemovePlugin).Assembly); ((MonoBehaviour)this).StartCoroutine(BootstrapWhenServerReady()); } private IEnumerator BootstrapWhenServerReady() { while ((Object)(object)ZNet.instance == (Object)null) { yield return null; } if (CE_Enable.Value && IsServer()) { yield return RunScanOnceCoroutine(); ((MonoBehaviour)this).StartCoroutine(ScanLoop()); } } private static bool IsServer() { try { return (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer(); } catch { return false; } } private IEnumerator ScanLoop() { while (true) { float num = Mathf.Max(5f, CE_ScanIntervalSeconds.Value); yield return (object)new WaitForSecondsRealtime(num); if (CE_Enable.Value && IsServer()) { yield return RunScanOnceCoroutine(); } } } 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)ZNet.instance != (Object)null && (ZNet.instance.IsAdmin(PlatformId.ToVanillaToken(steamId)) || ZNet.instance.IsAdmin(steamId.ToString(CultureInfo.InvariantCulture)))) { return true; } } catch { } } return false; } private static string FirstToken(string trimmed) { int i; for (i = 0; i < trimmed.Length && !char.IsWhiteSpace(trimmed[i]); i++) { } if (i > 0) { return trimmed.Substring(0, i); } return string.Empty; } private static string? TryGetSavedirFromCommandLine() { try { string[] commandLineArgs = Environment.GetCommandLineArgs(); for (int i = 0; i < commandLineArgs.Length; i++) { string text = commandLineArgs[i] ?? string.Empty; if (string.Equals(text, "-savedir", StringComparison.OrdinalIgnoreCase)) { if (i + 1 < commandLineArgs.Length) { string text2 = (commandLineArgs[i + 1] ?? string.Empty).Trim().Trim(new char[1] { '"' }); if (!string.IsNullOrWhiteSpace(text2)) { return text2; } } return null; } if (!text.StartsWith("-savedir", StringComparison.OrdinalIgnoreCase)) { continue; } int num = text.IndexOf('='); int num2 = text.IndexOf(':'); int num3 = -1; if (num >= 0) { num3 = num; } else if (num2 >= 0) { num3 = num2; } if (num3 >= 0 && num3 + 1 < text.Length) { string text3 = text.Substring(num3 + 1).Trim().Trim(new char[1] { '"' }); if (!string.IsNullOrWhiteSpace(text3)) { return text3; } } } } catch { } return null; } private static string GetVanillaListRootPath() { string text = TryGetSavedirFromCommandLine(); if (!string.IsNullOrWhiteSpace(text)) { return text; } return Application.persistentDataPath; } private IEnumerator RunScanOnceCoroutine() { RefreshBypassCacheIfNeeded(); string text = (CE_WhitelistFileName.Value ?? "permittedlist.txt").Trim(); if (string.IsNullOrWhiteSpace(text)) { yield break; } string vanillaListRootPath = GetVanillaListRootPath(); if (string.IsNullOrWhiteSpace(vanillaListRootPath)) { yield break; } Directory.CreateDirectory(vanillaListRootPath); string whitelistPath = Path.Combine(vanillaListRootPath, text); if (!File.Exists(whitelistPath)) { yield break; } long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); long threshold = (long)Mathf.Max(1f, CE_InactiveThresholdSeconds.Value); HashSet<ulong> online = OnlineSnapshot.GetOnlineSteamIds(); bool onlineSeenChanged = false; foreach (ulong item in online) { SeenStore.SetLastSeen(item, now); onlineSeenChanged = true; } string[] lines = Array.Empty<string>(); bool readOk = false; int readAttempt = 0; while (readAttempt < 3) { bool flag; try { lines = File.ReadAllLines(whitelistPath, Encoding.UTF8); readOk = true; } catch (IOException) { readAttempt++; if (readAttempt >= 3) { break; } flag = true; goto IL_016c; } catch { } break; IL_016c: if (flag) { yield return (object)new WaitForSecondsRealtime(0.2f); } } if (!readOk) { yield break; } List<ParsedLine> list = new List<ParsedLine>(lines.Length); for (int i = 0; i < lines.Length; i++) { string text2 = lines[i] ?? string.Empty; string text3 = text2.Trim(); if (text3.Length == 0 || text3.StartsWith("//", StringComparison.Ordinal) || text3.StartsWith("#", StringComparison.Ordinal) || text3.StartsWith(";", StringComparison.Ordinal)) { list.Add(new ParsedLine(text2, text3, isCommentOrEmpty: true, string.Empty, isNumericToken: false, 0uL)); continue; } string text4 = FirstToken(text3); ulong steamId; if (text4.Length == 0) { list.Add(new ParsedLine(text2, text3, isCommentOrEmpty: true, string.Empty, isNumericToken: false, 0uL)); } else if (PlatformId.TryGetSteamNumber(text4, out steamId)) { list.Add(new ParsedLine(text2, text3, isCommentOrEmpty: false, text4, isNumericToken: true, steamId)); } else { list.Add(new ParsedLine(text2, text3, isCommentOrEmpty: false, text4, isNumericToken: false, 0uL)); } } if (list.Count == 0) { yield break; } bool flag2 = onlineSeenChanged; int num = 0; Dictionary<ulong, long> pendingRemoval = new Dictionary<ulong, long>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); HashSet<ulong> keptUlong = new HashSet<ulong>(); for (int j = 0; j < list.Count; j++) { ParsedLine parsedLine = list[j]; if (!parsedLine.IsCommentOrEmpty && parsedLine.IsNumericToken) { ulong steamId2 = parsedLine.SteamId; long lastSeen; if (online.Contains(steamId2)) { hashSet.Add(parsedLine.Token); keptUlong.Add(steamId2); } else if (IsBypassed(steamId2)) { hashSet.Add(parsedLine.Token); keptUlong.Add(steamId2); } else if (!SeenStore.TryGetLastSeen(steamId2, out lastSeen)) { SeenStore.SetLastSeen(steamId2, now); flag2 = true; hashSet.Add(parsedLine.Token); keptUlong.Add(steamId2); } else if (now - lastSeen > threshold) { num++; pendingRemoval[steamId2] = lastSeen; } else { hashSet.Add(parsedLine.Token); keptUlong.Add(steamId2); } } } if (num == 0) { if (flag2) { SeenStore.Save(_lastSeenPath, ((BaseUnityPlugin)this).Logger); } yield break; } List<string> rebuilt = new List<string>(list.Count); for (int k = 0; k < list.Count; k++) { ParsedLine parsedLine2 = list[k]; if (parsedLine2.IsCommentOrEmpty) { rebuilt.Add(parsedLine2.Raw); } else if (!parsedLine2.IsNumericToken) { rebuilt.Add(parsedLine2.Raw); } else if (hashSet.Contains(parsedLine2.Token)) { rebuilt.Add(parsedLine2.Raw); } } bool writeOk = false; int writeAttempt = 0; while (writeAttempt < 3) { bool flag3; try { if (File.ReadAllLines(whitelistPath, Encoding.UTF8).SequenceEqual(lines)) { HashSet<ulong> onlineSteamIds = OnlineSnapshot.GetOnlineSteamIds(); if (!pendingRemoval.Keys.Any(onlineSteamIds.Contains)) { FileIoRetry.AtomicWriteText(whitelistPath, string.Join(Environment.NewLine, rebuilt) + Environment.NewLine); writeOk = true; } } } catch (IOException) { writeAttempt++; if (writeAttempt >= 3) { break; } flag3 = true; goto IL_055e; } catch { } break; IL_055e: if (flag3) { yield return (object)new WaitForSecondsRealtime(0.2f); } } if (!writeOk) { yield break; } foreach (KeyValuePair<ulong, long> item2 in pendingRemoval) { SeenStore.Remove(item2.Key); AppendRemovedLog(_removedLogPath, item2.Key, item2.Value, now - item2.Value, "inactive", ((BaseUnityPlugin)this).Logger); } SeenStore.PruneTo(keptUlong); SeenStore.Save(_lastSeenPath, ((BaseUnityPlugin)this).Logger); } private static void AppendRemovedLog(string removedLogPath, ulong steamId, long lastSeen, long inactiveSeconds, string reason, ManualLogSource logger) { 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 { } } } internal static class SeenStore { private static readonly object LockObj = new object(); private static readonly Dictionary<ulong, long> LastSeenUnixBySteamId = new Dictionary<ulong, long>(); private static bool _loaded; internal static void Load(string path, ManualLogSource logger) { lock (LockObj) { if (_loaded) { return; } _loaded = true; if (!File.Exists(path)) { return; } try { string[] array = File.ReadAllLines(path, Encoding.UTF8); for (int i = 0; i < array.Length; i++) { string text = (array[i] ?? string.Empty).Trim(); if (text.Length == 0 || text[0] != '"') { continue; } int num = text.IndexOf('"', 1); if (num <= 1) { continue; } string s = text.Substring(1, num - 1); int num2 = text.IndexOf(':', num + 1); if (num2 >= 0) { string s2 = text.Substring(num2 + 1).Trim().TrimEnd(new char[1] { ',' }); if (ulong.TryParse(s, NumberStyles.None, CultureInfo.InvariantCulture, out var result) && long.TryParse(s2, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2)) { LastSeenUnixBySteamId[result] = result2; } } } } catch { } } } 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 void MarkSeenNow(ulong steamId) { long unixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); SetLastSeen(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 void Save(string path, ManualLogSource logger) { lock (LockObj) { 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); } catch { } } } } 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); } if (ulong.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out steamId)) { return steamId != 0; } return false; } internal static string ToVanillaToken(ulong steamId) { return "V_" + steamId.ToString(CultureInfo.InvariantCulture); } } internal static class OnlineSnapshot { private static bool TryGetSteamId(ZNetPeer peer, out ulong steamId) { steamId = 0uL; try { if (peer == null) { return false; } ISocket socket = peer.m_socket; if (socket == null) { return false; } string text = (socket.GetHostName() ?? string.Empty).Trim(); if (text.Length == 0) { return false; } int num = text.IndexOf(':'); if (num >= 0) { text = text.Substring(0, num).Trim(); } if (text.Length == 0) { return false; } return PlatformId.TryGetSteamNumber(text, out steamId); } catch { return false; } } internal static HashSet<ulong> GetOnlineSteamIds() { HashSet<ulong> hashSet = new HashSet<ulong>(); try { if ((Object)(object)ZNet.instance == (Object)null) { return hashSet; } List<ZNetPeer> peers = ZNet.instance.GetPeers(); for (int i = 0; i < peers.Count; i++) { ZNetPeer val = peers[i]; if (val != null && TryGetSteamId(val, out var steamId)) { hashSet.Add(steamId); } } } catch { } return hashSet; } } internal static class FileIoRetry { internal const int MaxRetries = 3; internal const float RetryDelaySeconds = 0.2f; internal static void AtomicWriteText(string path, string content) { Directory.CreateDirectory(Path.GetDirectoryName(path) ?? "."); 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); } } private static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { } } } [HarmonyPatch] internal static class PeerSeenPatches { [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] [HarmonyPostfix] private static void ZNet_RPC_PeerInfo_Postfix(ZNet __instance, ZRpc rpc) { try { if ((Object)(object)__instance == (Object)null || !__instance.IsServer()) { return; } List<ZNetPeer> peers = __instance.GetPeers(); for (int i = 0; i < peers.Count; i++) { ZNetPeer val = peers[i]; if (val == null || val.m_rpc != rpc) { continue; } ISocket socket = val.m_socket; if (socket == null) { break; } string text = (socket.GetHostName() ?? string.Empty).Trim(); if (text.Length != 0) { int num = text.IndexOf(':'); if (num >= 0) { text = text.Substring(0, num).Trim(); } if (PlatformId.TryGetSteamNumber(text, out var steamId)) { SeenStore.MarkSeenNow(steamId); } } break; } } catch { } } } }