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