From 7e2511148786c209c818998a1b5212112462a09a Mon Sep 17 00:00:00 2001 From: Regalis11 Date: Wed, 22 Oct 2025 14:54:03 +0300 Subject: [PATCH 1/2] Release 1.10.7.2 - Autumn Update 2025 Hotfix 4 --- .../ClientSource/Networking/GameClient.cs | 5 +- .../Networking/ServerList/ServerInfo.cs | 108 +++++++++++-- .../ServerListScreen/ServerListScreen.cs | 64 ++++++-- .../ClientSource/Screens/SubEditorScreen.cs | 20 +-- .../ClientSource/SpamServerFilter.cs | 148 ++++++++++++++---- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/Networking/GameServer.cs | 7 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../SharedSource/Map/SubmarineInfo.cs | 3 + .../SharedSource/Networking/NetworkMember.cs | 2 + .../SharedSource/Utils/Homoglyphs.cs | 19 +++ .../SharedSource/Utils/Md5Hash.cs | 4 +- Barotrauma/BarotraumaShared/changelog.txt | 6 + 17 files changed, 315 insertions(+), 83 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 1a257fc3e..03958b5cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -3815,7 +3815,10 @@ namespace Barotrauma.Networking //let's adhere to those foreach (Submarine sub in Submarine.Loaded.Take(5)) { - string subNameTruncated = sub.Info.Name.Length > 16 ? sub.Info.Name.Substring(0, 16) : sub.Info.Name; + string subNameTruncated = + sub.Info.Name.Length > MaxSubNameLengthInErrorMessages ? + sub.Info.Name.Substring(0, MaxSubNameLengthInErrorMessages) : + sub.Info.Name; outMsg.WriteString(subNameTruncated); } break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs index 05376aa1b..a1835ed24 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/ServerInfo.cs @@ -26,11 +26,72 @@ namespace Barotrauma.Networking public Option MetadataSource = Option.None; + // Cached normalized versions of server strings for efficient homoglyph comparison + private string? cachedNormalizedName; + private string? cachedNormalizedMessage; + private string? cachedNormalizedGameMode; + + private string serverName = ""; + private string serverMessage = ""; + private Identifier gameMode = Identifier.Empty; + [Serialize("", IsPropertySaveable.Yes)] - public string ServerName { get; set; } = ""; - + public string ServerName + { + get { return serverName; } + set + { + serverName = value; + cachedNormalizedName = null; // Invalidate cache + } + } + [Serialize("", IsPropertySaveable.Yes)] - public string ServerMessage { get; set; } = ""; + public string ServerMessage + { + get { return serverMessage; } + set + { + serverMessage = value; + cachedNormalizedMessage = null; // Invalidate cache + } + } + + public string NormalizedServerName + { + get + { + if (cachedNormalizedName == null) + { + cachedNormalizedName = Homoglyphs.Normalize(ServerName); + } + return cachedNormalizedName; + } + } + + public string NormalizedServerMessage + { + get + { + if (cachedNormalizedMessage == null) + { + cachedNormalizedMessage = Homoglyphs.Normalize(ServerMessage); + } + return cachedNormalizedMessage; + } + } + + public string NormalizedGameMode + { + get + { + if (cachedNormalizedGameMode == null) + { + cachedNormalizedGameMode = Homoglyphs.Normalize(GameMode.Value); + } + return cachedNormalizedGameMode; + } + } public int PlayerCount { get; set; } @@ -43,8 +104,16 @@ namespace Barotrauma.Networking public bool HasPassword { get; set; } [Serialize("", IsPropertySaveable.Yes)] - public Identifier GameMode { get; set; } - + public Identifier GameMode + { + get { return gameMode; } + set + { + gameMode = value; + cachedNormalizedGameMode = null; // Invalidate cache + } + } + [Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)] public SelectionMode ModeSelectionMode { get; set; } @@ -541,14 +610,27 @@ namespace Barotrauma.Networking return Array.Empty(); } - return contentPackageNames - .Zip(contentPackageHashes, (name, hash) => (name, hash)) - .Zip(contentPackageIds, (t1, id) => - new ServerListContentPackageInfo( - t1.name, - t1.hash, - ContentPackageId.Parse(id))) - .ToArray(); + List contentPackageInfos = new List(); + for (int i = 0; i < contentPackageNames.Count; i++) + { + string name = contentPackageNames[i]; + string hash = contentPackageHashes[i]; + string ugcId = contentPackageIds[i]; + //according to Steam documentation, this is the maximum length of a workshop item title + //see k_cchPublishedDocumentTitleMax + if (name.Length > 128 + 1) + { + name = name.Substring(0, 128 + 1); + } + if (hash.Length > Md5Hash.MaxHashLength) + { + hash = hash.Substring(0, Md5Hash.MaxHashLength); + } + //note that we don't validate the UGC ID here, that should be handled in ContentPackageId.Parse + contentPackageInfos.Add(new ServerListContentPackageInfo(name, hash, ContentPackageId.Parse(ugcId))); + } + + return contentPackageInfos.ToArray(); } public static Option FromXElement(XElement element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index b3216cfe7..db7d383dd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -27,6 +27,10 @@ namespace Barotrauma private DateTime lastRefreshTime = DateTime.Now; + // Cache for spam-filtered servers to avoid re-checking on every filter change + private readonly HashSet spamServerCache = new HashSet(); + private readonly Dictionary serverInfoStringCache = new Dictionary(); + private GUIFrame menu; private GUIListBox serverList; @@ -1013,7 +1017,6 @@ namespace Barotrauma return false; } #endif - if (SpamServerFilters.IsFiltered(serverInfo)) { return false; } if (!string.IsNullOrEmpty(searchBox.Text) && !serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase)) { return false; } @@ -1216,6 +1219,11 @@ namespace Barotrauma PingUtils.QueryPingData(); + // Clear spam server cache to allow re-checking servers (user might have changed filters) + spamServerCache.Clear(); + // Also clear server info string cache when manually refreshing, just so we don't end up with broken data in any situation + serverInfoStringCache.Clear(); + tabs[TabEnum.All].Clear(); serverList.ClearChildren(); serverPreview.Content.ClearChildren(); @@ -1328,26 +1336,21 @@ namespace Barotrauma if (serverInfo.PlayerCount > serverInfo.MaxPlayers + 1) { return; } if (serverInfo.PlayerCount < 0) { return; } if (serverInfo.MaxPlayers <= 0) { return; } + if (!serverInfo.SelectedSub.IsNullOrEmpty()) + { + if (serverInfo.SelectedSub.Length > SubmarineInfo.MaxNameLength) { return; } + } //no way a legit server can have this many players if (serverInfo.MaxPlayers > MaxAllowedPlayers) { return; } - int similarServerCount = 0; - string serverInfoStr = getServerInfoStr(serverInfo); - foreach (var serverElement in serverList.Content.Children) + // Check spam filter with caching to avoid re-checking on every filter change + string serverCacheKey = serverInfo.Endpoints.First().StringRepresentation; + if (spamServerCache.Contains(serverCacheKey)) { return; } + if (SpamServerFilters.IsFiltered(serverInfo)) { - if (!serverElement.Visible) { continue; } - if (serverElement.UserData is not ServerInfo otherServer || otherServer == serverInfo) { continue; } - if (ToolBox.LevenshteinDistance(serverInfoStr, getServerInfoStr(otherServer)) < serverInfoStr.Length * (1.0f - MinSimilarityPercentage)) - { - similarServerCount++; - if (similarServerCount > MaxAllowedSimilarServers) - { - DebugConsole.Log($"Server {serverInfo.ServerName} seems to be almost identical to {otherServer.ServerName}. Hiding as a potential spam server."); - break; - } - } + spamServerCache.Add(serverCacheKey); + return; } - if (similarServerCount > MaxAllowedSimilarServers) { return; } static string getServerInfoStr(ServerInfo serverInfo) { @@ -1356,6 +1359,35 @@ namespace Barotrauma return str; } + string getCachedServerInfoStr(ServerInfo serverInfo) + { + string cacheKey = serverInfo.Endpoints.First().StringRepresentation; + if (!serverInfoStringCache.TryGetValue(cacheKey, out string cachedStr)) + { + cachedStr = getServerInfoStr(serverInfo); + serverInfoStringCache[cacheKey] = cachedStr; + } + return cachedStr; + } + + int similarServerCount = 0; + string serverInfoStr = getServerInfoStr(serverInfo); + foreach (var serverElement in serverList.Content.Children) + { + if (!serverElement.Visible) { continue; } + if (serverElement.UserData is not ServerInfo otherServer || otherServer == serverInfo) { continue; } + if (ToolBox.LevenshteinDistance(serverInfoStr, getCachedServerInfoStr(otherServer)) < serverInfoStr.Length * (1.0f - MinSimilarityPercentage)) + { + similarServerCount++; + if (similarServerCount > MaxAllowedSimilarServers) + { + DebugConsole.Log($"Server {serverInfo.ServerName} seems to be almost identical to {otherServer.ServerName}. Hiding as a potential spam server."); + break; + } + } + } + if (similarServerCount > MaxAllowedSimilarServers) { return; } + RemoveMsgFromServerList(MsgUserData.RefreshingServerList); RemoveMsgFromServerList(MsgUserData.NoServers); var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 0ae7c8114..5c23144d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -338,10 +338,7 @@ namespace Barotrauma private GUIImage previewImage; private GUILayoutGroup previewImageButtonHolder; - private const int submarineNameLimit = 30; private GUITextBlock submarineNameCharacterCount; - - private const int submarineDescriptionLimit = 500; private GUITextBlock submarineDescriptionCharacterCount; private Mode mode; @@ -2304,20 +2301,19 @@ namespace Barotrauma }; nameBox.OnTextChanged += (textBox, text) => { - if (text.Length > submarineNameLimit) + if (text.Length > SubmarineInfo.MaxNameLength) { - nameBox.Text = text.Substring(0, submarineNameLimit); + nameBox.Text = text.Substring(0, SubmarineInfo.MaxNameLength); nameBox.Flash(GUIStyle.Red); return true; } - submarineNameCharacterCount.Text = text.Length + " / " + submarineNameLimit; + submarineNameCharacterCount.Text = text.Length + " / " + SubmarineInfo.MaxNameLength; return true; }; nameBox.Text = MainSub?.Info.Name ?? ""; - - submarineNameCharacterCount.Text = nameBox.Text.Length + " / " + submarineNameLimit; + submarineNameCharacterCount.Text = nameBox.Text.Length + " / " + SubmarineInfo.MaxNameLength; var descriptionHeaderGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.03f), leftColumn.RectTransform), isHorizontal: true); @@ -2333,9 +2329,9 @@ namespace Barotrauma descriptionBox.OnTextChanged += (textBox, text) => { - if (text.Length > submarineDescriptionLimit) + if (text.Length > SubmarineInfo.MaxDescriptionLength) { - descriptionBox.Text = text.Substring(0, submarineDescriptionLimit); + descriptionBox.Text = text.Substring(0, SubmarineInfo.MaxDescriptionLength); descriptionBox.Flash(GUIStyle.Red); return true; } @@ -3439,7 +3435,7 @@ namespace Barotrauma enemySubmarineSettingsContainer.Recalculate(); descriptionBox.Text = MainSub == null ? "" : MainSub.Info.Description.Value; - submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; + submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + SubmarineInfo.MaxDescriptionLength; subTypeDropdown.SelectItem(MainSub.Info.Type); @@ -5045,7 +5041,7 @@ namespace Barotrauma textBox.UserData = text; } - submarineDescriptionCharacterCount.Text = text.Length + " / " + submarineDescriptionLimit; + submarineDescriptionCharacterCount.Text = text.Length + " / " + SubmarineInfo.MaxDescriptionLength; } private bool SelectPrefab(GUIComponent component, object obj) diff --git a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs index 9a7fceda2..2214848d4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SpamServerFilter.cs @@ -1,10 +1,12 @@ #nullable enable using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Net; using System.Net.Cache; +using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; @@ -20,8 +22,10 @@ namespace Barotrauma Invalid, NameEquals, NameContains, + NameMatchesRegex, MessageEquals, MessageContains, + MessageMatchesRegex, PlayerCountLarger, PlayerCountExact, MaxPlayersLarger, @@ -29,20 +33,21 @@ namespace Barotrauma GameModeEquals, PlayStyleEquals, Endpoint, - LanguageEquals + LanguageEquals, + LobbyId } - internal readonly record struct SpamFilter(ImmutableHashSet<(SpamServerFilterType Type, string Value)> Filters) + internal readonly record struct SpamFilter(ImmutableHashSet<(SpamServerFilterType Type, string Value, string NormalizedValue)> Filters) { public bool IsFiltered(ServerInfo info) { if (Filters.IsEmpty) { return false; } - foreach (var (type, value) in Filters) + foreach (var (type, value, normalizedValue) in Filters) { try { - if (!IsFiltered(info, type, value)) { return false; } + if (!IsFiltered(info, type, value, normalizedValue)) { return false; } } catch (Exception e) { @@ -53,57 +58,83 @@ namespace Barotrauma return true; } - private static bool IsFiltered(ServerInfo info, SpamServerFilterType type, string value) + private static bool IsFiltered(ServerInfo info, SpamServerFilterType type, string value, string normalizedValue) { if (info == null) { return true; } - string desc = info.ServerMessage, - name = info.ServerName; - int.TryParse(value, out int parsedInt); return type switch { - SpamServerFilterType.NameEquals => CompareEquals(name, value), - SpamServerFilterType.NameContains => CompareContains(name, value), + SpamServerFilterType.NameEquals => CompareEquals(info.NormalizedServerName, normalizedValue), + SpamServerFilterType.NameContains => CompareContains(info.NormalizedServerName, normalizedValue), + SpamServerFilterType.NameMatchesRegex => CompareRegex(info.NormalizedServerName, value), - SpamServerFilterType.MessageEquals => CompareEquals(desc, value), - SpamServerFilterType.MessageContains => CompareContains(desc, value), + SpamServerFilterType.MessageEquals => CompareEquals(info.NormalizedServerMessage, normalizedValue), + SpamServerFilterType.MessageContains => CompareContains(info.NormalizedServerMessage, normalizedValue), + SpamServerFilterType.MessageMatchesRegex => CompareRegex(info.NormalizedServerMessage, value), SpamServerFilterType.Endpoint => info.Endpoints != null && info.Endpoints.First().StringRepresentation.Equals(value, StringComparison.OrdinalIgnoreCase), + SpamServerFilterType.LobbyId => + info.MetadataSource.TryUnwrap(out var dataSource) && + dataSource is SteamP2PServerProvider.DataSource steamDataSource && + ulong.TryParse(value, out ulong lobbyIdToFilter) && + steamDataSource.Lobby.Id == lobbyIdToFilter, + SpamServerFilterType.PlayerCountLarger => info.PlayerCount > parsedInt, SpamServerFilterType.PlayerCountExact => info.PlayerCount == parsedInt, SpamServerFilterType.MaxPlayersLarger => info.MaxPlayers > parsedInt, SpamServerFilterType.MaxPlayersExact => info.MaxPlayers == parsedInt, - SpamServerFilterType.GameModeEquals => info.GameMode == value, + SpamServerFilterType.GameModeEquals => CompareEquals(info.NormalizedGameMode, normalizedValue), SpamServerFilterType.PlayStyleEquals => info.PlayStyle.ToIdentifier() == value, SpamServerFilterType.LanguageEquals => info.Language.Value == value, _ => false }; - static bool CompareEquals(string a, string b) + static bool CompareEquals(string? normalizedA, string? normalizedB) { - if (a == null || b == null) + if (normalizedA == null || normalizedB == null) { - return a == b; + return normalizedA == normalizedB; } - return a.Equals(b, StringComparison.OrdinalIgnoreCase) || Homoglyphs.Compare(a, b); - + // Both strings are already normalized, just do case-insensitive comparison + return normalizedA.Equals(normalizedB, StringComparison.OrdinalIgnoreCase); } - static bool CompareContains(string a, string b) + static bool CompareContains(string? normalizedA, string? normalizedB) { - if (a == null || b == null) + if (normalizedA == null || normalizedB == null) { - return a == b; + return normalizedA == normalizedB; } - return a.Contains(b, StringComparison.OrdinalIgnoreCase); + // Both strings are already normalized, just do case-insensitive contains + return normalizedA.Contains(normalizedB, StringComparison.OrdinalIgnoreCase); + } + + static bool CompareRegex(string? a, string? pattern) + { + if (a == null || pattern == null) + { + return a == pattern; + } + + // Use cached compiled regex for performance + if (SpamServerFilters.GetCachedRegex(pattern) is Regex regex) + { + return regex.IsMatch(a); + } + else + { + DebugConsole.ThrowError($"Regex pattern somehow not found in cache: \"{pattern}\""); + } + + return false; } } @@ -111,7 +142,7 @@ namespace Barotrauma { var element = new XElement("Filter"); - foreach (var (type, value) in Filters) + foreach (var (type, value, _) in Filters) { element.Add(new XAttribute(type.ToString().ToLowerInvariant(), value)); } @@ -121,7 +152,7 @@ namespace Barotrauma public static bool TryParse(XElement element, out SpamFilter filter) { - var builder = ImmutableHashSet.CreateBuilder<(SpamServerFilterType Type, string Value)>(); + var builder = ImmutableHashSet.CreateBuilder<(SpamServerFilterType Type, string Value, string NormalizedValue)>(); foreach (var attribute in element.Attributes()) { if (!Enum.TryParse(attribute.Name.ToString(), ignoreCase: true, out SpamServerFilterType e)) @@ -130,7 +161,25 @@ namespace Barotrauma continue; } if (e is SpamServerFilterType.Invalid) { continue; } - builder.Add((e, attribute.Value)); + string value = attribute.Value; + + // Compile regex patterns during loading (for validation and performance) + if (e is SpamServerFilterType.NameMatchesRegex or SpamServerFilterType.MessageMatchesRegex) + { + // Skip invalid regex filters (will throw error to the log though) + if (!SpamServerFilters.TryCompileAndCacheRegex(value)) { continue; } + } + + // Only normalize values for filter types that actually use homoglyph comparison + string normalizedValue = e is SpamServerFilterType.NameEquals + or SpamServerFilterType.NameContains + or SpamServerFilterType.MessageEquals + or SpamServerFilterType.MessageContains + or SpamServerFilterType.GameModeEquals + ? Homoglyphs.Normalize(value) + : value; + + builder.Add((e, value, normalizedValue)); } if (builder.Any()) @@ -207,6 +256,37 @@ namespace Barotrauma public static Option LocalSpamFilter; public static Option GlobalSpamFilter; + private static readonly Dictionary CompiledRegexCache = new Dictionary(); + + /// + /// Attempts to compile a regex pattern and cache it. Returns false if the pattern is invalid. + /// Compilation validates the regex is correct, avoiding crashes at runtime, and subsequent use will be more performant. + /// + internal static bool TryCompileAndCacheRegex(string pattern) + { + if (CompiledRegexCache.ContainsKey(pattern)) { return true; } + + try + { + var regex = new Regex(pattern, RegexOptions.Compiled); + CompiledRegexCache[pattern] = regex; + return true; + } + catch (Exception e) + { + DebugConsole.ThrowError($"Invalid regex pattern in spam filter: \"{pattern}\"", e); + return false; + } + } + + /// + /// Attempts to get a cached compiled regex, returns null if not found. + /// + internal static Regex? GetCachedRegex(string pattern) + { + return CompiledRegexCache.GetValueOrDefault(pattern); + } + public const string LocalFilterComment = @" This file contains a list of filters that can be used to hide servers from the server list. You can add filters by right-clicking a server in the server list and selecting ""Hide server"" or by reporting the server and choosing ""Report and hide server"". @@ -214,28 +294,34 @@ The filters are saved in this file, which you can edit manually if you want to. The available filter types are: - NameEquals: The server name must equal the specified value. Homoglyphs are also checked. -- NameContains: The server name must contain the specified value. +- NameContains: The server name must contain the specified value. Homoglyphs are also checked. +- NameMatchesRegex: The server name must match the specified regular expression pattern. Use inline options like (?i) for case-insensitive matching. - MessageEquals: The server description must equal the specified value. Homoglyphs are also checked. -- MessageContains: The server description must contain the specified value. +- MessageContains: The server description must contain the specified value. Homoglyphs are also checked. +- MessageMatchesRegex: The server description must match the specified regular expression pattern. Use inline options like (?i) for case-insensitive matching. - PlayerCountLarger: The player count must be larger than the specified value. - PlayerCountExact: The player count must match the specified value exactly. - MaxPlayersLarger: The max player count must be larger than the specified value. - MaxPlayersExact: The max player count must match the specified value exactly. -- GameModeEquals: The game mode identifier must match the specified value exactly. +- GameModeEquals: The game mode identifier must match the specified value exactly. Homoglyphs are also checked. - PlayStyleEquals: The play style must match the specified value exactly. - Endpoint: The server endpoint, which is a Steam ID or an IP address, must match the specified value exactly. Steam ID is in the format of STEAM_X:Y:Z. - LanguageEquals: The server language must match the specified value exactly. +- LobbyId: The Steam lobby ID must match the specified value exactly. This is the most efficient way to filter Steam P2P lobbies, when we have already identified harmful ones. The filter values are case-insensitive and adding multiple conditions on one filter will require all of them to be met. -Homoglyph comparison is used for NameEquals and MessageEquals filters, which means that it checks whether the words look the same, meaning you can't abuse identical-looking but different symbols to work around the filter. For example ""lmaobox"" and ""lmаobox"" (with a cyrillic a) are considered equal. +Homoglyph comparison is used for name, message, and game mode filters, which means that it checks whether the words look the same, meaning you can't abuse identical-looking but different symbols to work around the filter. For example ""lmaobox"" and ""lmаobox"" (with a cyrillic a) are considered equal, and ""dіscord.gg"" (with a cyrillic i) will be caught by a ""discord.gg"" contains filter. Examples: + + + -These will hide all servers that have a discord.gg link in their name or description and servers with the name ""get good get lmaobox"" that have 999 max players. +These will hide all servers that have a discord.gg link in their name or description, servers with the name ""get good get lmaobox"" that have 999 max players, the specific lobby with ID 109775241070418378, servers with names matching the pattern for buying/selling/trading (case-insensitive), and servers with messages containing discord links (case-insensitive).. "; static SpamServerFilters() { @@ -279,7 +365,7 @@ These will hide all servers that have a discord.gg link in their name or descrip if (!LocalSpamFilter.TryUnwrap(out var localFilter)) { return; } if (localFilter.IsFiltered(info)) { return; } - var filters = localFilter.Filters.Add(new SpamFilter(ImmutableHashSet.Create((NameExact: SpamServerFilterType.NameEquals, info.ServerName)))); + var filters = localFilter.Filters.Add(new SpamFilter(ImmutableHashSet.Create((SpamServerFilterType.NameEquals, info.ServerName, info.NormalizedServerName)))); var newFilter = new SpamServerFilter(filters); newFilter.Save(SpamServerFilter.SavePath); LocalSpamFilter = Option.Some(newFilter); diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index ee31d4352..a68dc51af 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.10.7.1 + 1.10.7.2 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 1bce93f2a..586fa6762 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.10.7.1 + 1.10.7.2 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index ff9579d1a..a3091d0de 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 1.10.7.1 + 1.10.7.2 Copyright © FakeFish 2018-2024 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 730e935d4..10088790e 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.10.7.1 + 1.10.7.2 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 9e5a66019..9d0d45d13 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.10.7.1 + 1.10.7.2 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 9ef3d021e..67bf3d68c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1071,7 +1071,7 @@ namespace Barotrauma.Networking for (int i = 0; i < Math.Min(subCount, 5); i++) { string subName = inc.ReadString(); - if (subName == null || subName.Length > 16) + if (subName == null || subName.Length > MaxSubNameLengthInErrorMessages) { malformedData = true; } @@ -1092,7 +1092,7 @@ namespace Barotrauma.Networking } else if (entity is Item item) { - errorStr = errorStrNoName = $"Missing item {item.Name}, sub: {item.Submarine?.Info?.Name ?? "none"} (event id {eventID}, entity id {entityID})."; + errorStr = errorStrNoName = $"Missing item {item.Name} ({item.Prefab.Identifier}), sub: {item.Submarine?.Info?.Name ?? "none"} (event id {eventID}, entity id {entityID})."; } else { @@ -1100,7 +1100,8 @@ namespace Barotrauma.Networking } if (GameStarted) { - var serverSubNames = Submarine.Loaded.Select(s => s.Info.Name); + var serverSubNames = Submarine.Loaded.Select(s => + s.Info.Name.Length > MaxSubNameLengthInErrorMessages ? s.Info.Name.Substring(0, MaxSubNameLengthInErrorMessages) : s.Info.Name); if (subCount != Submarine.Loaded.Count || !subNames.SequenceEqual(serverSubNames)) { string subErrorStr = $" Loaded submarines don't match (client: {string.Join(", ", subNames)}, server: {string.Join(", ", serverSubNames)})."; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 5e1fd9707..c854dd0a3 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 1.10.7.1 + 1.10.7.2 Copyright © FakeFish 2018-2023 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 239b38056..e760df983 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -63,6 +63,9 @@ namespace Barotrauma public HashSet RequiredContentPackages = new HashSet(); + public const int MaxNameLength = 30; + public const int MaxDescriptionLength = 500; + public string Name { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 59fed5243..3850825f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -181,6 +181,8 @@ namespace Barotrauma.Networking abstract partial class NetworkMember { + protected const int MaxSubNameLengthInErrorMessages = 16; + public UInt16 LastClientListUpdateID { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Homoglyphs.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Homoglyphs.cs index b7c975946..3db3a2cdd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Homoglyphs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Homoglyphs.cs @@ -1874,5 +1874,24 @@ namespace Barotrauma } return true; } + + /// + /// Normalizes a string by replacing all homoglyph characters with their ASCII equivalents. + /// This allows for efficient homoglyph-aware string comparisons using standard string operations. + /// + public static string Normalize(string input) + { + if (string.IsNullOrEmpty(input)) { return input; } + + var normalized = new char[input.Length]; + for (int i = 0; i < input.Length; i++) + { + uint charCode = (uint)input[i]; + uint[] glyphGroup = homoglyphs.Find(g => g.Contains(charCode)); + // The first element in each homoglyph group is the canonical ASCII character + normalized[i] = glyphGroup != null ? (char)glyphGroup[0] : input[i]; + } + return new string(normalized); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs index c672b5745..d788c182b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs @@ -11,7 +11,9 @@ namespace Barotrauma { public class Md5Hash { - public static readonly Md5Hash Blank = new Md5Hash(new string('0', 32)); + public const int MaxHashLength = 32; + + public static readonly Md5Hash Blank = new Md5Hash(new string('0', MaxHashLength)); private static string RemoveWhitespace(string s) { diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index cbc56da81..184af8542 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,4 +1,10 @@ ------------------------------------------------------------------------------------------------------------------------------------------------- +v1.10.7.2 +------------------------------------------------------------------------------------------------------------------------------------------------- + +- Improvements to the spam server filtering. + +------------------------------------------------------------------------------------------------------------------------------------------------- v1.10.7.1 ------------------------------------------------------------------------------------------------------------------------------------------------- From 51db93fabcb4751b11b79b8f55e6ef3c5f9afec9 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Wed, 22 Oct 2025 14:57:25 +0300 Subject: [PATCH 2/2] Update bug-reports.yml Updated version options in bug report template. --- .github/DISCUSSION_TEMPLATE/bug-reports.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/bug-reports.yml b/.github/DISCUSSION_TEMPLATE/bug-reports.yml index b9d0e2b27..9c1a5ee94 100644 --- a/.github/DISCUSSION_TEMPLATE/bug-reports.yml +++ b/.github/DISCUSSION_TEMPLATE/bug-reports.yml @@ -73,7 +73,8 @@ body: label: Version description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu. options: - - v1.10.7.1 (Autumn Update 2025 Hotfix 3) + - v1.10.7.2 (Autumn Update 2025 Hotfix 4) + - v1.11.0.0 (Unstable) - Other validations: required: true