Merge branch 'master' of https://github.com/regalis11/Barotrauma
This commit is contained in:
3
.github/DISCUSSION_TEMPLATE/bug-reports.yml
vendored
3
.github/DISCUSSION_TEMPLATE/bug-reports.yml
vendored
@@ -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
|
||||
|
||||
@@ -3822,7 +3822,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;
|
||||
|
||||
@@ -26,11 +26,72 @@ namespace Barotrauma.Networking
|
||||
|
||||
public Option<DataSource> 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<ServerListContentPackageInfo>();
|
||||
}
|
||||
|
||||
return contentPackageNames
|
||||
.Zip(contentPackageHashes, (name, hash) => (name, hash))
|
||||
.Zip(contentPackageIds, (t1, id) =>
|
||||
new ServerListContentPackageInfo(
|
||||
t1.name,
|
||||
t1.hash,
|
||||
ContentPackageId.Parse(id)))
|
||||
.ToArray();
|
||||
List<ServerListContentPackageInfo> contentPackageInfos = new List<ServerListContentPackageInfo>();
|
||||
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<ServerInfo> FromXElement(XElement element)
|
||||
|
||||
@@ -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<string> spamServerCache = new HashSet<string>();
|
||||
private readonly Dictionary<string, string> serverInfoStringCache = new Dictionary<string, string>();
|
||||
|
||||
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) },
|
||||
|
||||
@@ -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;
|
||||
@@ -2306,20 +2303,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);
|
||||
|
||||
@@ -2335,9 +2331,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;
|
||||
}
|
||||
@@ -3441,7 +3437,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);
|
||||
|
||||
@@ -5047,7 +5043,7 @@ namespace Barotrauma
|
||||
textBox.UserData = text;
|
||||
}
|
||||
|
||||
submarineDescriptionCharacterCount.Text = text.Length + " / " + submarineDescriptionLimit;
|
||||
submarineDescriptionCharacterCount.Text = text.Length + " / " + SubmarineInfo.MaxDescriptionLength;
|
||||
}
|
||||
|
||||
private bool SelectPrefab(GUIComponent component, object obj)
|
||||
|
||||
@@ -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<SpamServerFilter> LocalSpamFilter;
|
||||
public static Option<SpamServerFilter> GlobalSpamFilter;
|
||||
|
||||
private static readonly Dictionary<string, Regex> CompiledRegexCache = new Dictionary<string, Regex>();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get a cached compiled regex, returns null if not found.
|
||||
/// </summary>
|
||||
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:
|
||||
<Filters>
|
||||
<Filter namecontains=""discord.gg"" />
|
||||
<Filter messagecontains=""discord.gg"" />
|
||||
<Filter nameequals=""get good get lmaobox"" maxplayersexact=""999"" />
|
||||
<Filter lobbyid=""109775241070418378"" />
|
||||
<Filter namematchesregex=""(?i)(buy|sell|trade).*cheap"" />
|
||||
<Filter messagematchesregex=""(?i)join.*discord\.(gg|com)"" />
|
||||
</Filters>
|
||||
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);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>Barotrauma</RootNamespace>
|
||||
<Authors>FakeFish, Undertow Games</Authors>
|
||||
<Product>Barotrauma</Product>
|
||||
<Version>1.10.7.1</Version>
|
||||
<Version>1.10.7.2</Version>
|
||||
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<AssemblyName>Barotrauma</AssemblyName>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>Barotrauma</RootNamespace>
|
||||
<Authors>FakeFish, Undertow Games</Authors>
|
||||
<Product>Barotrauma</Product>
|
||||
<Version>1.10.7.1</Version>
|
||||
<Version>1.10.7.2</Version>
|
||||
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<AssemblyName>Barotrauma</AssemblyName>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>Barotrauma</RootNamespace>
|
||||
<Authors>FakeFish, Undertow Games</Authors>
|
||||
<Product>Barotrauma</Product>
|
||||
<Version>1.10.7.1</Version>
|
||||
<Version>1.10.7.2</Version>
|
||||
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<AssemblyName>Barotrauma</AssemblyName>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>Barotrauma</RootNamespace>
|
||||
<Authors>FakeFish, Undertow Games</Authors>
|
||||
<Product>Barotrauma Dedicated Server</Product>
|
||||
<Version>1.10.7.1</Version>
|
||||
<Version>1.10.7.2</Version>
|
||||
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<AssemblyName>DedicatedServer</AssemblyName>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>Barotrauma</RootNamespace>
|
||||
<Authors>FakeFish, Undertow Games</Authors>
|
||||
<Product>Barotrauma Dedicated Server</Product>
|
||||
<Version>1.10.7.1</Version>
|
||||
<Version>1.10.7.2</Version>
|
||||
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<AssemblyName>DedicatedServer</AssemblyName>
|
||||
|
||||
@@ -1083,7 +1083,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;
|
||||
}
|
||||
@@ -1104,7 +1104,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
|
||||
{
|
||||
@@ -1112,7 +1112,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)}).";
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>Barotrauma</RootNamespace>
|
||||
<Authors>FakeFish, Undertow Games</Authors>
|
||||
<Product>Barotrauma Dedicated Server</Product>
|
||||
<Version>1.10.7.1</Version>
|
||||
<Version>1.10.7.2</Version>
|
||||
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<AssemblyName>DedicatedServer</AssemblyName>
|
||||
|
||||
@@ -63,6 +63,9 @@ namespace Barotrauma
|
||||
|
||||
public HashSet<string> RequiredContentPackages = new HashSet<string>();
|
||||
|
||||
public const int MaxNameLength = 30;
|
||||
public const int MaxDescriptionLength = 500;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
|
||||
@@ -184,6 +184,8 @@ namespace Barotrauma.Networking
|
||||
|
||||
abstract partial class NetworkMember
|
||||
{
|
||||
protected const int MaxSubNameLengthInErrorMessages = 16;
|
||||
|
||||
public UInt16 LastClientListUpdateID
|
||||
{
|
||||
get;
|
||||
|
||||
@@ -1874,5 +1874,24 @@ namespace Barotrauma
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a string by replacing all homoglyph characters with their ASCII equivalents.
|
||||
/// This allows for efficient homoglyph-aware string comparisons using standard string operations.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
v1.10.7.2
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
- Improvements to the spam server filtering.
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
v1.10.7.1
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user