From bb1999b20e39e435c0ea78f77b02c42a5fb8aaf2 Mon Sep 17 00:00:00 2001 From: Rushaway Date: Thu, 3 Sep 2026 10:55:59 +0200 Subject: [PATCH 1/2] fix(plugin): ban by engine-supported method for Synergy compatibility The SB++ plugin enforced bans of already-connected / reconnecting players and temp-ban queue retries by sending "banid STEAM_X:Y:Z" to the server console. Some engines (notably Synergy, appid 17520) reject the STEAM_ format for that command ("STEAM_ format is not supported for this command"), so the ban was silently dropped and the player kept rejoining. Route these paths through BanClient() when the target is still connected so SourceMod selects a ban method the running engine actually supports, and fall back to "banid" with the SteamID3 form when the player is offline. Fixes sbpp/sourcebans-pp#1507 Co-Authored-By: Claude Sonnet 5 --- game/addons/sourcemod/scripting/sbpp_main.sp | 73 ++++++++++++++++---- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/game/addons/sourcemod/scripting/sbpp_main.sp b/game/addons/sourcemod/scripting/sbpp_main.sp index 75746aa6c..2454f61cb 100644 --- a/game/addons/sourcemod/scripting/sbpp_main.sp +++ b/game/addons/sourcemod/scripting/sbpp_main.sp @@ -1624,8 +1624,7 @@ public void AddedFromSQLiteCallback(Database db, DBResultSet results, const char } else { // the insert failed so we leave the record in the queue and increase our temporary ban - FormatEx(buffer, sizeof(buffer), "banid %d %s", ProcessQueueTime, auth); - ServerCommand(buffer); + SBPP_BanIdentity(auth, ProcessQueueTime); } delete dataPack; } @@ -1692,7 +1691,7 @@ public void VerifyBan(Database db, DBResultSet results, const char[] error, int if (results.RowCount > 0) { - char buffer[40], Name[MAX_NAME_LENGTH], Query[512]; + char Name[MAX_NAME_LENGTH], Query[512]; // Amending to ban record's IP field if (results.FetchRow()) @@ -1731,9 +1730,12 @@ public void VerifyBan(Database db, DBResultSet results, const char[] error, int db.Query(ErrorCheckCallback, Query, client, DBPrio_High); - FormatEx(buffer, sizeof(buffer), "banid 5 %s", clientAuth); - ServerCommand(buffer); - KickClient(client, "%t", "Banned Check Site", WebsiteAddress); + // Ban via BanClient() so SourceMod uses an engine-supported ban method. + // The raw "banid " console command is rejected by some + // engines (e.g. Synergy), which left the player unbanned. + char BanReason[256]; + FormatEx(BanReason, sizeof(BanReason), "%t", "Banned Check Site", WebsiteAddress); + BanClient(client, 5, BANFLAG_AUTHID, BanReason, BanReason, "sbpp"); return; } @@ -2595,11 +2597,7 @@ stock void UTIL_InsertTempBan(int time, const char[] name, const char[] auth, co delete dataPack; // we add a temporary ban and then add the record into the queue to be processed when the database is available - char buffer[50]; - - Format(buffer, sizeof(buffer), "banid %d %s", ProcessQueueTime, auth); - - ServerCommand(buffer); + char kickMessage[512]; if (IsClientInGame(client)) { @@ -2608,9 +2606,11 @@ stock void UTIL_InsertTempBan(int time, const char[] name, const char[] auth, co FormatEx(length, sizeof(length), "permanent"); else FormatEx(length, sizeof(length), "%d %s", time, time == 1 ? "minute" : "minutes"); - KickClient(client, "%t\n\n%t", "Banned Check Site", WebsiteAddress, "Kick Reason", admin, reason, length); + FormatEx(kickMessage, sizeof(kickMessage), "%t\n\n%t", "Banned Check Site", WebsiteAddress, "Kick Reason", admin, reason, length); } + SBPP_BanIdentity(auth, ProcessQueueTime, kickMessage); + char banName[MAX_NAME_LENGTH], banReason[256], query[512]; SQLiteDB.Escape(name, banName, sizeof(banName)); @@ -2838,4 +2838,53 @@ stock void AccountForLateLoading() } } +/** + * Converts a SteamID2 ("STEAM_X:Y:Z") into a SteamID3 ("[U:1:W]"). + * + * @return true on success, false if the input was not a SteamID2. + */ +stock bool SBPP_Steam2ToSteam3(const char[] steam2, char[] buffer, int maxlen) +{ + if (strncmp(steam2, "STEAM_", 6) != 0) + return false; + + char parts[3][12]; + if (ExplodeString(steam2[6], ":", parts, sizeof(parts), sizeof(parts[])) != 3) + return false; + + int y = StringToInt(parts[1]); + int z = StringToInt(parts[2]); + FormatEx(buffer, maxlen, "[U:1:%d]", (z * 2) + y); + return true; +} + +/** + * Bans a player by authid regardless of whether they are still connected. + * + * Uses BanClient() when the player is in game so SourceMod picks the ban + * method the running engine actually supports. Some engines (e.g. Synergy, + * appid 17520) reject the "STEAM_" format passed to the "banid" console + * command, which silently dropped the ban. When the player is offline we + * fall back to "banid" using the SteamID3 format for the same reason. + */ +stock void SBPP_BanIdentity(const char[] auth, int minutes, const char[] kickMessage = "") +{ + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && IsClientAuthorized(i) && StrEqual(g_sSteamIDs[i], auth, false)) + { + BanClient(i, minutes, BANFLAG_AUTHID, kickMessage, kickMessage, "sbpp"); + return; + } + } + + char cmd[64], steam3[MAX_AUTHID_LENGTH]; + if (SBPP_Steam2ToSteam3(auth, steam3, sizeof(steam3))) + FormatEx(cmd, sizeof(cmd), "banid %d \"%s\"", minutes, steam3); + else + FormatEx(cmd, sizeof(cmd), "banid %d %s", minutes, auth); + + ServerCommand(cmd); +} + //Yarr! From 6895d2ce13e2f172388f1569328520600757286e Mon Sep 17 00:00:00 2001 From: Rushaway Date: Thu, 3 Sep 2026 14:13:59 +0200 Subject: [PATCH 2/2] review fixes Follow-ups on the Synergy engine-ban fix: - Restore per-client translation. KickClient() set the global translation target before formatting; FormatEx() does not, so "Banned Check Site" / "Kick Reason" were rendered against whatever target happened to be set last. Added SetGlobalTransTarget() in VerifyBan() and UTIL_InsertTempBan(). - SBPP_BanIdentity(): take an optional client hint (UTIL_InsertTempBan already knows the target), skip fake clients so BanClient() cannot throw "Cannot ban fake client", bail on an empty authid, and clamp minutes to >= 1 so a misconfigured ProcessQueueTime cannot turn a temporary hold into a permanent ban written to banned_user.cfg. - SBPP_BanIdentity(): build the localised "Banned Check Site" message when no kick message is supplied, instead of letting SourceMod kick with "Kicked" (hit by the AddedFromSQLiteCallback retry path). - Use the BanIdentity() native for the offline path rather than a hand-built ServerCommand("banid ..."): it strips command separators from the identity and fires OnBanIdentity, matching the RemoveBan() call already used nearby. - SBPP_Steam2ToSteam3(): reject STEAM_ID_PENDING / STEAM_ID_LAN and empty fields, and render the account id with %u so ids past 2^31 do not come out negative. Co-Authored-By: Claude Sonnet 5 --- game/addons/sourcemod/scripting/sbpp_main.sp | 109 +++++++++++++++---- 1 file changed, 87 insertions(+), 22 deletions(-) diff --git a/game/addons/sourcemod/scripting/sbpp_main.sp b/game/addons/sourcemod/scripting/sbpp_main.sp index 2454f61cb..d793f32fd 100644 --- a/game/addons/sourcemod/scripting/sbpp_main.sp +++ b/game/addons/sourcemod/scripting/sbpp_main.sp @@ -1730,10 +1730,13 @@ public void VerifyBan(Database db, DBResultSet results, const char[] error, int db.Query(ErrorCheckCallback, Query, client, DBPrio_High); - // Ban via BanClient() so SourceMod uses an engine-supported ban method. - // The raw "banid " console command is rejected by some - // engines (e.g. Synergy), which left the player unbanned. + // Ban via BanClient() so SourceMod bans with the engine's own auth + // string. The raw "banid " console command is rejected by + // some engines (e.g. Synergy), which left the player unbanned. + // SetGlobalTransTarget() keeps the message in the client's language, + // which KickClient() used to do for us. char BanReason[256]; + SetGlobalTransTarget(client); FormatEx(BanReason, sizeof(BanReason), "%t", "Banned Check Site", WebsiteAddress); BanClient(client, 5, BANFLAG_AUTHID, BanReason, BanReason, "sbpp"); @@ -2597,7 +2600,7 @@ stock void UTIL_InsertTempBan(int time, const char[] name, const char[] auth, co delete dataPack; // we add a temporary ban and then add the record into the queue to be processed when the database is available - char kickMessage[512]; + char kickMessage[512] = ""; if (IsClientInGame(client)) { @@ -2606,10 +2609,12 @@ stock void UTIL_InsertTempBan(int time, const char[] name, const char[] auth, co FormatEx(length, sizeof(length), "permanent"); else FormatEx(length, sizeof(length), "%d %s", time, time == 1 ? "minute" : "minutes"); + // KickClient() used to localise this for us; keep doing so. + SetGlobalTransTarget(client); FormatEx(kickMessage, sizeof(kickMessage), "%t\n\n%t", "Banned Check Site", WebsiteAddress, "Kick Reason", admin, reason, length); } - SBPP_BanIdentity(auth, ProcessQueueTime, kickMessage); + SBPP_BanIdentity(auth, ProcessQueueTime, kickMessage, client); char banName[MAX_NAME_LENGTH], banReason[256], query[512]; @@ -2841,50 +2846,110 @@ stock void AccountForLateLoading() /** * Converts a SteamID2 ("STEAM_X:Y:Z") into a SteamID3 ("[U:1:W]"). * - * @return true on success, false if the input was not a SteamID2. + * The universe is always rendered as 1 (public), mirroring how SourceMod + * itself renders AuthId_Steam3: mods that set "UseInvalidUniverseInSteam2IDs" + * render public-universe accounts as STEAM_0, so the X field is not a + * trustworthy universe. + * + * @return true on success, false if the input was not a convertible SteamID2. */ stock bool SBPP_Steam2ToSteam3(const char[] steam2, char[] buffer, int maxlen) { if (strncmp(steam2, "STEAM_", 6) != 0) return false; + // "STEAM_ID_PENDING" / "STEAM_ID_LAN" carry no account id. + if (strncmp(steam2[6], "ID_", 3) == 0) + return false; + char parts[3][12]; if (ExplodeString(steam2[6], ":", parts, sizeof(parts), sizeof(parts[])) != 3) return false; + if (parts[1][0] == '\0' || parts[2][0] == '\0') + return false; + int y = StringToInt(parts[1]); int z = StringToInt(parts[2]); - FormatEx(buffer, maxlen, "[U:1:%d]", (z * 2) + y); + + // %u, not %d: account ids past 2^31 would otherwise render negative. + FormatEx(buffer, maxlen, "[U:1:%u]", (z * 2) + y); return true; } /** * Bans a player by authid regardless of whether they are still connected. * - * Uses BanClient() when the player is in game so SourceMod picks the ban - * method the running engine actually supports. Some engines (e.g. Synergy, - * appid 17520) reject the "STEAM_" format passed to the "banid" console - * command, which silently dropped the ban. When the player is offline we - * fall back to "banid" using the SteamID3 format for the same reason. + * Uses BanClient() when the player is in game so SourceMod bans with the + * engine's own auth string (SourceMod feeds "banid" the string the engine + * itself reports for that player). Some engines (e.g. Synergy, appid 17520) + * reject the "STEAM_" format passed to the "banid" console command, which + * silently dropped the ban. When the player is offline we ban by identity + * using the SteamID3 form for the same reason. + * + * @param auth SteamID2 of the target. + * @param minutes Ban length in minutes. + * @param kickMessage Message shown to the target if still connected; the + * generic "check the website" phrase is used when empty. + * @param client Optional client index hint for the target. */ -stock void SBPP_BanIdentity(const char[] auth, int minutes, const char[] kickMessage = "") +stock void SBPP_BanIdentity(const char[] auth, int minutes, const char[] kickMessage = "", int client = 0) { - for (int i = 1; i <= MaxClients; i++) + if (auth[0] == '\0') + return; + + // These are always temporary holds until the database catches up, so never + // let a misconfigured length through: "banid 0" is a permanent ban and + // SourceMod additionally writes it out to banned_user.cfg. + if (minutes < 1) + minutes = 5; + + int target = 0; + + if (client > 0 && client <= MaxClients && IsClientInGame(client) && !IsFakeClient(client) + && StrEqual(g_sSteamIDs[client], auth, false)) { - if (IsClientInGame(i) && IsClientAuthorized(i) && StrEqual(g_sSteamIDs[i], auth, false)) + target = client; + } + else + { + for (int i = 1; i <= MaxClients; i++) { - BanClient(i, minutes, BANFLAG_AUTHID, kickMessage, kickMessage, "sbpp"); - return; + if (IsClientInGame(i) && !IsFakeClient(i) && StrEqual(g_sSteamIDs[i], auth, false)) + { + target = i; + break; + } } } - char cmd[64], steam3[MAX_AUTHID_LENGTH]; + if (target) + { + char message[512]; + + if (kickMessage[0] != '\0') + { + strcopy(message, sizeof(message), kickMessage); + } + else + { + SetGlobalTransTarget(target); + FormatEx(message, sizeof(message), "%t", "Banned Check Site", WebsiteAddress); + } + + // BanClient() kicks on the next frame, so callers passing a global + // buffer (g_sSteamIDs[] & co.) can keep using it after this returns. + BanClient(target, minutes, BANFLAG_AUTHID, message, message, "sbpp"); + return; + } + + // BanIdentity() rather than a hand-built ServerCommand("banid ..."): it + // strips command separators out of the identity and notifies OnBanIdentity. + char steam3[MAX_AUTHID_LENGTH]; if (SBPP_Steam2ToSteam3(auth, steam3, sizeof(steam3))) - FormatEx(cmd, sizeof(cmd), "banid %d \"%s\"", minutes, steam3); + BanIdentity(steam3, minutes, BANFLAG_AUTHID, kickMessage, "sbpp"); else - FormatEx(cmd, sizeof(cmd), "banid %d %s", minutes, auth); - - ServerCommand(cmd); + BanIdentity(auth, minutes, BANFLAG_AUTHID, kickMessage, "sbpp"); } //Yarr!