Merge pull request #98 from At0mBomba/move-to-regex-filters

Move to full regex support for filter lists
This commit is contained in:
D1GQ authored and GitHub committed 2026-08-21 16:14:32 -05:00
commit 7e8415df57
7 files changed
+238 -125

No files matched your search

+200 -60
View File
@@ -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");
/// <summary>
/// File containing banned words/patterns.
/// Legacy file containing banned words/patterns.
/// </summary>
internal static readonly string banWordListFilePath = Path.Combine(Folders.saveInfoFolderPath, "BanWordList.txt");
internal static readonly string banWordListFilePath_Legacy = Path.Combine(Folders.saveInfoFolderPath, "BanWordList.txt");
/// <summary>
/// File containing banned chat messages/patterns.
/// </summary>
internal static readonly string banChatListFilePath = Path.Combine(Folders.saveInfoFolderPath, "BanChatList.txt");
}
/// <summary>
/// Contains initial content for all files that require initialization.
/// </summary>
private static class Contents
{
/// <summary>
/// Example comment for BanPlayerList file.
/// </summary>
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
""";
/// <summary>
/// Example comment for BanNameList file.
/// </summary>
internal const string BAN_NAME_LIST_CONTENT = """
// Example banned player name regex patterns
// Each pattern on a new line
//
// ^TNT$
// ^hyde$
// ^[\P{IsBasicLatin}\s]+$
""";
/// <summary>
/// Example comment for BanChatList file.
/// </summary>
internal const string BAN_CHAT_LIST_CONTENT = """
// Example banned chat message regex patterns
// Each pattern on a new line
//
// (?i)(?<!(?:say|said|don'?t|didn'?t|wr[io]te|\bthe\b|\bat\b).*)(?: |^)sta*r?t*(?:ing|\b)(?!.*ban)
// (?i)^go[go]*$");
// (?i)ni?gg?(?:a|er)
// (?i)sns(?-i).*[A-Z]{6}
// [A-Z]{6}.*(?i)sns
// (?i)(?:modded|expert|no cooldown) lobby(?-i).*[A-Z]{6}
// [A-Z]{6}.*(?i)(?:modded|expert|no cooldown) lobby
""";
/// <summary>
/// Legacy example comment for BanNameList file.
/// </summary>
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**
""";
}
/// <summary>
@@ -114,7 +187,7 @@ internal static class BetterDataManager
[
Files.banPlayerListFilePath,
Files.banNameListFilePath,
Files.banWordListFilePath
Files.banChatListFilePath
];
/// <summary>
@@ -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();
}
/// <summary>
/// Performs data migration from legacy wildcard‑based ban lists to the new regex‑based format.
/// </summary>
private static void HandleMigrationFromWildcardToRegex()
{
MigrateBanWordList();
MigrateBanNameList();
}
/// <summary>
/// Migrates the legacy wildcard‑based BanWordList to the new regex-based BanChatList.
/// </summary>
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);
}
/// <summary>
/// Migrates the BanNameList from the legacy wildcard format to the current regex‑based format.
/// </summary>
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();
}
}
/// <summary>
/// Determines whether any line from the supplied array matches a line in the specified content string.
/// </summary>
/// <param name="lines">An array of strings to search.</param>
/// <param name="content">A multi‑line string whose individual lines are compared against <paramref name="lines"/>.</param>
/// <returns><c>true</c> if at least one line from <paramref name="lines"/> equals any line in <paramref name="content"/>, <c>false</c> otherwise.</returns>
private static bool AnyLineInContentLines(this string[] lines, string content)
{
return content.Split(Environment.NewLine).Any(lines.Contains);
}
/// <summary>
@@ -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!");
}
}
}
+15 -52
View File
@@ -9,25 +9,14 @@ namespace BetterAmongUs.Modules;
internal static class TextFileHandler
{
/// <summary>
/// Compares strings against filter patterns in a file using wildcard matching.
/// Compares a string against regex patterns in a file.
/// </summary>
/// <param name="filePath">Path to the filter file.</param>
/// <param name="strings">Array of strings to check against filters.</param>
/// <returns>True if any string matches a filter pattern, false otherwise.</returns>
internal static bool CompareStringFilters(string filePath, string[] strings)
/// <param name="filePath">Path to the file containing regex patterns.</param>
/// <param name="inputString">String to match against.</param>
/// <returns>True if the string matches a regex pattern, false otherwise.</returns>
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));
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -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 [];
}
/// <summary>
/// Checks if a text matches a filter pattern with wildcard support.
/// </summary>
/// <param name="filter">The filter pattern (supports ** wildcards).</param>
/// <param name="text">The text to check.</param>
/// <returns>True if the text matches the filter pattern, false otherwise.</returns>
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);
}
/// <summary>
/// Parses a YAML-like string into a dictionary of key-value pairs.
/// </summary>
@@ -74,7 +74,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_BanNameListMessage.LocalizedString, bypassDataCheck: true);
}
@@ -16,8 +16,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;
@@ -75,8 +76,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
+3 -2
View File
@@ -88,8 +88,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",
+8 -3
View File
@@ -434,14 +434,19 @@ public static class TranslationStrings
public static readonly TranslationString BetterSetting_Setting_UseBanNameList = new("BetterSetting.Setting.UseBanNameList");
/// <summary>
/// Base Translation: Use ban word list
/// Base Translation: Use ban chat list
/// </summary>
public static readonly TranslationString BetterSetting_Setting_UseBanWordList = new("BetterSetting.Setting.UseBanWordList");
public static readonly TranslationString BetterSetting_Setting_UseBanChatList = new("BetterSetting.Setting.UseBanChatList");
/// <summary>
/// Base Translation: Only in lobby
/// </summary>
public static readonly TranslationString BetterSetting_Setting_UseBanWordListOnlyLobby = new("BetterSetting.Setting.UseBanWordListOnlyLobby");
public static readonly TranslationString BetterSetting_Setting_UseBanChatListOnlyLobby = new("BetterSetting.Setting.UseBanChatListOnlyLobby");
/// <summary>
/// Base Translation: Ban
/// </summary>
public static readonly TranslationString BetterSetting_Setting_UseBanChatListBan = new("BetterSetting.Setting.UseBanChatListBan");
/// <summary>
/// Base Translation: Show role in name for clients