From 63a2bcbbe6f7c935a7179816cf8a79400889df26 Mon Sep 17 00:00:00 2001 From: At0mBomba <102352627+At0mBomba@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:37:32 +0300 Subject: [PATCH] Move to regex for filter lists --- src/Data/BetterDataManager.cs | 260 ++++++++++++++---- .../AntiCheat/RPCHandlers/SendChatHandler.cs | 7 +- src/Modules/TextFileHandler.cs | 67 +---- .../Gameplay/Player/PlayerJoinAndLeftPatch.cs | 3 +- .../Gameplay/UI/Settings/GameSettingsPatch.cs | 10 +- src/Resources/Lang/en_US.json | 5 +- src/build/Generated/TranslationStrings.cs | 11 +- 7 files changed, 238 insertions(+), 125 deletions(-) diff --git a/src/Data/BetterDataManager.cs b/src/Data/BetterDataManager.cs index 78ad325..fad8c7e 100644 --- a/src/Data/BetterDataManager.cs +++ b/src/Data/BetterDataManager.cs @@ -1,4 +1,5 @@ -using BetterAmongUs.Data.Config; +using System.Text.RegularExpressions; +using BetterAmongUs.Data.Config; using BetterAmongUs.Data.Json; using BetterAmongUs.Utilities; using UnityEngine; @@ -102,9 +103,81 @@ internal static class BetterDataManager internal static readonly string banNameListFilePath = Path.Combine(Folders.saveInfoFolderPath, "BanNameList.txt"); /// - /// File containing banned words/patterns. + /// Legacy file containing banned words/patterns. /// - internal static readonly string banWordListFilePath = Path.Combine(Folders.saveInfoFolderPath, "BanWordList.txt"); + internal static readonly string banWordListFilePath_Legacy = Path.Combine(Folders.saveInfoFolderPath, "BanWordList.txt"); + + /// + /// File containing banned chat messages/patterns. + /// + internal static readonly string banChatListFilePath = Path.Combine(Folders.saveInfoFolderPath, "BanChatList.txt"); + } + + /// + /// Contains initial content for all files that require initialization. + /// + private static class Contents + { + /// + /// Example comment for BanPlayerList file. + /// + internal const string BAN_PLAYER_LIST_CONTENT = """ + // Example ban entries (friend code and/or hashed PUID) + // Format: [FriendCode], [HashedPUID] + // Example with both: + // FriendCode#0000, abc123def456789 + // Example with just friend code: + // FriendCode#0000 + // Example with just hashed PUID: + // , hash123xyz789 + """; + + /// + /// Example comment for BanNameList file. + /// + internal const string BAN_NAME_LIST_CONTENT = """ + // Example banned player name regex patterns + // Each pattern on a new line + // + // ^TNT$ + // ^hyde$ + // ^[\P{IsBasicLatin}\s]+$ + """; + + /// + /// Example comment for BanChatList file. + /// + internal const string BAN_CHAT_LIST_CONTENT = """ + // Example banned chat message regex patterns + // Each pattern on a new line + // + // (?i)(? + /// Legacy example comment for BanNameList file. + /// + internal const string BAN_NAME_LIST_CONTENT_LEGACY = """ + // Example banned player names + // Each name on a new line - supports wildcards with ** + // ** at start and end: contains anywhere + // ** at start only: ends with + // ** at end only: starts with + // No **: exact match (case-insensitive) + // + // HackerPlayer123 + // CheaterAccount + // **Bot** + // **Script + // Exploit** + // **Cheat** + """; } /// @@ -114,7 +187,7 @@ internal static class BetterDataManager [ Files.banPlayerListFilePath, Files.banNameListFilePath, - Files.banWordListFilePath + Files.banChatListFilePath ]; /// @@ -169,63 +242,26 @@ internal static class BetterDataManager foreach (var path in InitPaths) { - if (!File.Exists(path)) - { - var directory = Path.GetDirectoryName(path); - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } + if (File.Exists(path)) continue; - using var writer = File.CreateText(path); - if (path == Files.banPlayerListFilePath) - { - writer.WriteLine("// Example ban entries (friend code and/or hashed PUID)"); - writer.WriteLine("// Format: [FriendCode], [HashedPUID]"); - writer.WriteLine("// Example with both:"); - writer.WriteLine("// FriendCode#0000, abc123def456789"); - writer.WriteLine("// Example with just friend code:"); - writer.WriteLine("// FriendCode#0000"); - writer.WriteLine("// Example with just hashed PUID:"); - writer.WriteLine("// , hash123xyz789"); - } - else if (path == Files.banNameListFilePath) - { - writer.WriteLine("// Example banned player names"); - writer.WriteLine("// Each name on a new line - supports wildcards with **"); - writer.WriteLine("// ** at start and end: contains anywhere"); - writer.WriteLine("// ** at start only: ends with"); - writer.WriteLine("// ** at end only: starts with"); - writer.WriteLine("// No **: exact match (case-insensitive)"); - writer.WriteLine("// "); - writer.WriteLine("// HackerPlayer123"); - writer.WriteLine("// CheaterAccount"); - writer.WriteLine("// **Bot**"); - writer.WriteLine("// **Script"); - writer.WriteLine("// Exploit**"); - writer.WriteLine("// **Cheat**"); - } - else if (path == Files.banWordListFilePath) - { - writer.WriteLine("// Example banned words/patterns"); - writer.WriteLine("// Each word or pattern on a new line - supports wildcards with **"); - writer.WriteLine("// ** at start and end: contains anywhere"); - writer.WriteLine("// ** at start only: ends with"); - writer.WriteLine("// ** at end only: starts with"); - writer.WriteLine("// No **: exact match (case-insensitive)"); - writer.WriteLine("// "); - writer.WriteLine("// hack"); - writer.WriteLine("// cheat"); - writer.WriteLine("// exploit"); - writer.WriteLine("// **bot**"); - writer.WriteLine("// **script**"); - writer.WriteLine("// modded"); - writer.WriteLine("// aimbot"); - writer.WriteLine("// wallhack"); - writer.WriteLine("// **hack**"); - writer.WriteLine("// **cheat**"); - writer.WriteLine("// speed**"); - } + var directory = Path.GetDirectoryName(path); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + using var writer = File.CreateText(path); + if (path == Files.banPlayerListFilePath) + { + writer.WriteLine(Contents.BAN_PLAYER_LIST_CONTENT); + } + else if (path == Files.banNameListFilePath) + { + writer.WriteLine(Contents.BAN_NAME_LIST_CONTENT); + } + else if (path == Files.banChatListFilePath) + { + writer.WriteLine(Contents.BAN_CHAT_LIST_CONTENT); } } } @@ -245,6 +281,110 @@ internal static class BetterDataManager { File.Move(Files.dataFilePath_Legacy, Files.dataFilePath); } + + HandleMigrationFromWildcardToRegex(); + } + + /// + /// Performs data migration from legacy wildcard‑based ban lists to the new regex‑based format. + /// + private static void HandleMigrationFromWildcardToRegex() + { + MigrateBanWordList(); + MigrateBanNameList(); + } + + /// + /// Migrates the legacy wildcard‑based BanWordList to the new regex-based BanChatList. + /// + private static void MigrateBanWordList() + { + if (!File.Exists(Files.banWordListFilePath_Legacy)) return; + + var content = File.ReadLines(Files.banWordListFilePath_Legacy) + .Where(line => + !string.IsNullOrWhiteSpace(line) && + !line.StartsWith("//") && + !line.StartsWith("#") + ).ToArray(); + + if (content.Any()) + { + using var writer = File.CreateText(Files.banChatListFilePath); + writer.WriteLine(Contents.BAN_CHAT_LIST_CONTENT); + writer.WriteLine(); + writer.WriteLine("// Your old wildcard based filters have been converted to regex patterns"); + writer.WriteLine("// They will behave exactly the same as before"); + writer.WriteLine("// You can keep using them without changing anything else"); + writer.WriteLine(); + foreach (var line in content) + { + writer.Write("(?i)(?: |^)"); + if (line.StartsWith("**")) writer.Write(".*"); + writer.Write(Regex.Escape(line.Trim('*'))); + if (line.EndsWith("**")) writer.Write(".*"); + writer.WriteLine("(?: |$)"); + } + } + File.Delete(Files.banWordListFilePath_Legacy); + } + + /// + /// Migrates the BanNameList from the legacy wildcard format to the current regex‑based format. + /// + private static void MigrateBanNameList() + { + if (!File.Exists(Files.banNameListFilePath)) return; + + var lines = File.ReadLines(Files.banNameListFilePath) + .Where(line => line != "// ") + .ToArray(); + + // Return if file contains new example comment + if (lines.AnyLineInContentLines(Contents.BAN_NAME_LIST_CONTENT)) + return; + + var content = lines.Where(line => + !string.IsNullOrWhiteSpace(line) && + !line.StartsWith("//") && + !line.StartsWith("#") + ).ToArray(); + + if (!content.Any()) + { + File.Delete(Files.banNameListFilePath); + return; + } + + if (!content.Any(s => Regex.IsMatch(s, @"\*\*")) && + !lines.AnyLineInContentLines(Contents.BAN_NAME_LIST_CONTENT_LEGACY)) return; + + using var writer = File.CreateText(Files.banNameListFilePath); + writer.WriteLine(Contents.BAN_NAME_LIST_CONTENT); + writer.WriteLine(); + writer.WriteLine("// Your old wildcard based filters have been converted to regex patterns"); + writer.WriteLine("// They will behave exactly the same as before"); + writer.WriteLine("// You can keep using them without changing anything else"); + writer.WriteLine(); + foreach (var line in content) + { + writer.Write("(?i)"); + if (!line.StartsWith("**")) writer.Write("^"); + writer.Write(Regex.Escape(line.Trim('*'))); + if (!line.EndsWith("**")) writer.Write("$"); + writer.WriteLine(); + } + } + + /// + /// Determines whether any line from the supplied array matches a line in the specified content string. + /// + /// An array of strings to search. + /// A multi‑line string whose individual lines are compared against . + /// true if at least one line from equals any line in , false otherwise. + private static bool AnyLineInContentLines(this string[] lines, string content) + { + return content.Split(Environment.NewLine).Any(lines.Contains); } /// diff --git a/src/Modules/AntiCheat/RPCHandlers/SendChatHandler.cs b/src/Modules/AntiCheat/RPCHandlers/SendChatHandler.cs index 720fd5b..2afad4e 100644 --- a/src/Modules/AntiCheat/RPCHandlers/SendChatHandler.cs +++ b/src/Modules/AntiCheat/RPCHandlers/SendChatHandler.cs @@ -17,11 +17,12 @@ internal sealed class SendChatHandler : RPCHandler { var text = reader.ReadString(); - if (BetterGameSettings.UseBanWordList.GetBool() && (!BetterGameSettings.UseBanWordListOnlyLobby.GetBool() || GameState.IsLobby)) + if (BetterGameSettings.UseBanChatList.GetBool() && (!BetterGameSettings.UseBanChatListOnlyLobby.GetBool() || GameState.IsLobby)) { - if (TextFileHandler.CompareStringFilters(BetterDataManager.Files.banWordListFilePath, text.Split(' '))) + if (TextFileHandler.CompareStringRegexMatches(BetterDataManager.Files.banChatListFilePath, text)) { - sender.Kick(false, $"has been kicked due to\nchat message containing a banned word!"); + var ban = BetterGameSettings.UseBanChatListBan.GetBool(); + sender.Kick(ban, $"has been {(ban ? "banned" : "kicked")} due to\nchat message matching a banned pattern!"); } } } diff --git a/src/Modules/TextFileHandler.cs b/src/Modules/TextFileHandler.cs index 658d100..6e94f63 100644 --- a/src/Modules/TextFileHandler.cs +++ b/src/Modules/TextFileHandler.cs @@ -9,25 +9,14 @@ namespace BetterAmongUs.Modules; internal static class TextFileHandler { /// - /// Compares strings against filter patterns in a file using wildcard matching. + /// Compares a string against regex patterns in a file. /// - /// Path to the filter file. - /// Array of strings to check against filters. - /// True if any string matches a filter pattern, false otherwise. - internal static bool CompareStringFilters(string filePath, string[] strings) + /// Path to the file containing regex patterns. + /// String to match against. + /// True if the string matches a regex pattern, false otherwise. + internal static bool CompareStringRegexMatches(string filePath, string inputString) { - foreach (var content in ReadContents(filePath)) - { - foreach (var text in strings) - { - if (CheckFilterString(content, text)) - { - return true; - } - } - } - - return false; + return ReadContents(filePath).Any(content => Regex.IsMatch(inputString, content, RegexOptions.Compiled)); } /// @@ -43,17 +32,10 @@ internal static class TextFileHandler StringComparer.OrdinalIgnoreCase ); - foreach (var content in ReadContents(filePath)) - { - string normalizedContent = content.ToLower().Trim(); - - if (stringSet.Contains(normalizedContent)) - { - return true; - } - } - - return false; + var contents = ReadContents(filePath) + .SelectMany(line => line.Split(',', StringSplitOptions.RemoveEmptyEntries)) + .Select(s => s.ToLower().Trim()); + return contents.Any(stringSet.Contains); } /// @@ -66,35 +48,16 @@ internal static class TextFileHandler if (File.Exists(filePath)) { return File.ReadLines(filePath) - .Where(line => !string.IsNullOrWhiteSpace(line) && - !line.StartsWith("//") && - !line.StartsWith("#")) - .SelectMany(line => line.Split(',', StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim())); + .Where(line => + !string.IsNullOrWhiteSpace(line) && + !line.StartsWith("//") && + !line.StartsWith("#") + ); } return []; } - /// - /// Checks if a text matches a filter pattern with wildcard support. - /// - /// The filter pattern (supports ** wildcards). - /// The text to check. - /// True if the text matches the filter pattern, false otherwise. - private static bool CheckFilterString(string filter, string text) - { - string pattern = filter switch - { - _ when filter.StartsWith("**") && filter.EndsWith("**") => Regex.Escape(filter.Trim('*')), // Contains anywhere - _ when filter.StartsWith("**") => Regex.Escape(filter.TrimStart('*')) + "$", // Ends with - _ when filter.EndsWith("**") => "^" + Regex.Escape(filter.TrimEnd('*')), // Starts with - _ => "^" + Regex.Escape(filter) + "$" // Exact match - }; - - return Regex.IsMatch(text, pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); - } - /// /// Parses a YAML-like string into a dictionary of key-value pairs. /// diff --git a/src/Patches/Gameplay/Player/PlayerJoinAndLeftPatch.cs b/src/Patches/Gameplay/Player/PlayerJoinAndLeftPatch.cs index 99b5da5..4e8ba86 100644 --- a/src/Patches/Gameplay/Player/PlayerJoinAndLeftPatch.cs +++ b/src/Patches/Gameplay/Player/PlayerJoinAndLeftPatch.cs @@ -73,7 +73,8 @@ internal static class PlayerJoinAndLeftPatch { if (player != null) { - if (TextFileHandler.CompareStringFilters(BetterDataManager.Files.banNameListFilePath, [player.Data.PlayerName])) + if (TextFileHandler.CompareStringRegexMatches(BetterDataManager.Files.banNameListFilePath, + player.Data.PlayerName)) { player?.Kick(true, TranslationStrings.AntiCheat_BanPlayerListMessage.LocalizedString, bypassDataCheck: true); } diff --git a/src/Patches/Gameplay/UI/Settings/GameSettingsPatch.cs b/src/Patches/Gameplay/UI/Settings/GameSettingsPatch.cs index 05417a2..8c619fd 100644 --- a/src/Patches/Gameplay/UI/Settings/GameSettingsPatch.cs +++ b/src/Patches/Gameplay/UI/Settings/GameSettingsPatch.cs @@ -15,8 +15,9 @@ internal sealed class BetterGameSettings internal static OptionCheckboxItem? InvalidFriendCode; internal static OptionCheckboxItem? UseBanPlayerList; internal static OptionCheckboxItem? UseBanNameList; - internal static OptionCheckboxItem? UseBanWordList; - internal static OptionCheckboxItem? UseBanWordListOnlyLobby; + internal static OptionCheckboxItem? UseBanChatList; + internal static OptionCheckboxItem? UseBanChatListOnlyLobby; + internal static OptionCheckboxItem? UseBanChatListBan; internal static OptionCheckboxItem? DetectedLevel; internal static OptionIntItem? DetectedLevelAbove; internal static OptionCheckboxItem? KickLevel; @@ -73,8 +74,9 @@ internal static class GameSettingsPatch BetterGameSettings.CancelInvalidSabotage = OptionCheckboxItem.Create(BetterSettingsTab, TranslationStrings.BetterSetting_Setting_CancelInvalidSabotage, true); BetterGameSettings.UseBanPlayerList = OptionCheckboxItem.Create(BetterSettingsTab, TranslationStrings.BetterSetting_Setting_UseBanPlayerList, true); BetterGameSettings.UseBanNameList = OptionCheckboxItem.Create(BetterSettingsTab, TranslationStrings.BetterSetting_Setting_UseBanNameList, true); - BetterGameSettings.UseBanWordList = OptionCheckboxItem.Create(BetterSettingsTab, TranslationStrings.BetterSetting_Setting_UseBanWordList, true); - BetterGameSettings.UseBanWordListOnlyLobby = OptionCheckboxItem.Create(BetterSettingsTab, TranslationStrings.BetterSetting_Setting_UseBanWordListOnlyLobby, true, BetterGameSettings.UseBanWordList); + BetterGameSettings.UseBanChatList = OptionCheckboxItem.Create(BetterSettingsTab, TranslationStrings.BetterSetting_Setting_UseBanChatList, true); + BetterGameSettings.UseBanChatListOnlyLobby = OptionCheckboxItem.Create(BetterSettingsTab, TranslationStrings.BetterSetting_Setting_UseBanChatListOnlyLobby, true, BetterGameSettings.UseBanChatList); + BetterGameSettings.UseBanChatListBan = OptionCheckboxItem.Create(BetterSettingsTab, TranslationStrings.BetterSetting_Setting_UseBanChatListBan, false, BetterGameSettings.UseBanChatList); } // General detection settings diff --git a/src/Resources/Lang/en_US.json b/src/Resources/Lang/en_US.json index 3042bc2..b43d84b 100644 --- a/src/Resources/Lang/en_US.json +++ b/src/Resources/Lang/en_US.json @@ -87,8 +87,9 @@ "BetterSetting.Setting.CancelInvalidSabotage": "Cancel invalid sabotages", "BetterSetting.Setting.UseBanPlayerList": "Use ban player list", "BetterSetting.Setting.UseBanNameList": "Use ban name list", - "BetterSetting.Setting.UseBanWordList": "Use ban word list", - "BetterSetting.Setting.UseBanWordListOnlyLobby": "Only in lobby", + "BetterSetting.Setting.UseBanChatList": "Use ban chat list", + "BetterSetting.Setting.UseBanChatListOnlyLobby": "Only in lobby", + "BetterSetting.Setting.UseBanChatListBan": "Ban", "BetterSetting.Setting.ShowRoleForClients": "Show role in name for clients", "BetterSetting.Setting.CensorDetectionReason": "Censor detection reason", "BetterSetting.Setting.DetectedLevel": "Detected player levels", diff --git a/src/build/Generated/TranslationStrings.cs b/src/build/Generated/TranslationStrings.cs index 9878aeb..67b39d8 100644 --- a/src/build/Generated/TranslationStrings.cs +++ b/src/build/Generated/TranslationStrings.cs @@ -429,14 +429,19 @@ public static class TranslationStrings public static readonly TranslationString BetterSetting_Setting_UseBanNameList = new("BetterSetting.Setting.UseBanNameList"); /// - /// Base Translation: Use ban word list + /// Base Translation: Use ban chat list /// - public static readonly TranslationString BetterSetting_Setting_UseBanWordList = new("BetterSetting.Setting.UseBanWordList"); + public static readonly TranslationString BetterSetting_Setting_UseBanChatList = new("BetterSetting.Setting.UseBanChatList"); /// /// Base Translation: Only in lobby /// - public static readonly TranslationString BetterSetting_Setting_UseBanWordListOnlyLobby = new("BetterSetting.Setting.UseBanWordListOnlyLobby"); + public static readonly TranslationString BetterSetting_Setting_UseBanChatListOnlyLobby = new("BetterSetting.Setting.UseBanChatListOnlyLobby"); + + /// + /// Base Translation: Ban + /// + public static readonly TranslationString BetterSetting_Setting_UseBanChatListBan = new("BetterSetting.Setting.UseBanChatListBan"); /// /// Base Translation: Show role in name for clients