Skip to content

Commit eda2a45

Browse files
Sigibertrumblefrog
andauthored
feat: add SBPP_BanPlayerBySteamId native for offline player bans (#1511)
* feat: add SBPP_BanPlayerBySteamId native for offline player bans SBPP_BanPlayer requires an in-game client index, making it impossible to ban players who disconnect before an automated ban fires. The new native accepts a SteamID string and player name directly, allowing plugins to record a ban in the SBPP database for players who are no longer connected. Implementation: - Checks for an existing active ban first (duplicate-prevention SELECT) - Inserts into sb_bans with length in seconds (iTime * 60) and the IFNULL admin-lookup subquery, matching the pattern used by CommandAddBan - Uses a server-lookup subquery when serverID == -1 (unregistered), or the cached serverID directly - Fires SBPP_OnBanPlayer with iTarget = -1 to signal an offline ban sourcebanspp.inc: - Declares the new native with full doc comment - Adds MarkNativeAsOptional in __pl_sourcebanspp_SetNTVOptional * fix: validate SteamID2 format before inserting ban SBPP enforces bans on connect using AuthId_Steam2. If a caller passed a SteamID64 or SteamID3 the ban would be stored but never fire. Now throws a native error immediately if the steamId argument does not start with 'STEAM_'. Addresses review comment from rumblefrog on PR #1511. * fix: only fire SBPP_OnBanPlayer after the ban INSERT succeeds Addresses review nit on PR #1511 (rumblefrog): the forward was fired synchronously right after issuing the async duplicate-check SELECT, before the ban row was ever written. It now fires from DB_OnBanBySteamIdInsert only when the INSERT query succeeds. * fix: harden offline SteamID ban native --------- Co-authored-by: rumblefrog <contact@zhenyangli.me>
1 parent efcc068 commit eda2a45

6 files changed

Lines changed: 215 additions & 7 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ Legend:
116116
`SBPP_OnClientPostAdminCheck` (#1431). Plugins that only call
117117
existing SBPP ban/report forwards/natives can usually keep their
118118
existing `.smx`. ([#1114](https://github.com/sbpp/sourcebans-pp/issues/1114))
119+
37. * `SBPP_BanPlayerBySteamId` SourceMod native for adding a ban after
120+
the target disconnects. This raises `SOURCEBANSPP_VERSION_MINOR`
121+
to `1` (#1511).
119122
```
120123

121124
### Privacy

‎game/addons/sourcemod/scripting/include/sbpp_version.inc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
#define _sbpp_version_included
1212

1313
#define MAJOR_REVISION 2
14-
#define MINOR_REVISION 0
14+
#define MINOR_REVISION 1
1515

1616
#define SOURCEBANSPP_VERSION_MAJOR MAJOR_REVISION
1717
#define SOURCEBANSPP_VERSION_MINOR MINOR_REVISION

‎game/addons/sourcemod/scripting/include/sourcebanspp.inc‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
// 2.0 epoch (this header):
4242
// sourcebanspp.inc — library "sourcebans++" (sbpp_main.smx)
4343
// Additive: forward SBPP_OnClientPostAdminCheck (#1431).
44+
// Additive: native SBPP_BanPlayerBySteamId (#1511).
4445
// Unchanged: SBPP_BanPlayer; SBBanPlayer remains deprecated.
4546
// Existing third-party .smx that only use prior natives/forwards
4647
// generally keep working without a rebuild.
@@ -81,6 +82,7 @@ public void __pl_sourcebanspp_SetNTVOptional()
8182
{
8283
MarkNativeAsOptional("SBBanPlayer");
8384
MarkNativeAsOptional("SBPP_BanPlayer");
85+
MarkNativeAsOptional("SBPP_BanPlayerBySteamId");
8486
MarkNativeAsOptional("SBPP_ReportPlayer");
8587
}
8688
#endif
@@ -109,6 +111,20 @@ native void SBBanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason
109111
*********************************************************/
110112
native void SBPP_BanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason);
111113

114+
/*********************************************************
115+
* Ban a player by SteamID — supports offline/disconnected players.
116+
* Inserts the ban directly into the SourceBans++ database.
117+
* Fires SBPP_OnBanPlayer with iTarget = -1 to indicate an offline ban.
118+
*
119+
* @param iAdmin Client index of the admin (0 = server / automated)
120+
* @param steamId SteamID string, e.g. "STEAM_0:0:12345"
121+
* @param name Player name; may be empty string if unknown
122+
* @param iTime Ban duration in minutes (0 = permanent)
123+
* @param sReason Reason for the ban
124+
* @noreturn
125+
*********************************************************/
126+
native void SBPP_BanPlayerBySteamId(int iAdmin, const char[] steamId, const char[] name, int iTime, const char[] sReason);
127+
112128
/*********************************************************
113129
* Reports a player
114130
*
@@ -123,7 +139,7 @@ native void SBPP_ReportPlayer(int iReporter, int iTarget, const char[] sReason);
123139
* Called when the admin banning the player.
124140
*
125141
* @param iAdmin The client index of the admin who is banning the client
126-
* @param iTarget The client index of the player to ban
142+
* @param iTarget The client index of the player to ban (-1 for an offline SteamID ban)
127143
* @param iTime The time to ban the player for (in minutes, 0 = permanent)
128144
* @param sReason The reason to ban the player from the server
129145
*********************************************************/

‎game/addons/sourcemod/scripting/sbpp_main.sp‎

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max
148148

149149
CreateNative("SBBanPlayer", Native_SBBanPlayer);
150150
CreateNative("SBPP_BanPlayer", Native_SBBanPlayer);
151+
CreateNative("SBPP_BanPlayerBySteamId", Native_SBPP_BanPlayerBySteamId);
151152
CreateNative("SBPP_ReportPlayer", Native_SBReportPlayer);
152153

153154
g_hFwd_OnBanAdded = CreateGlobalForward("SBPP_OnBanPlayer", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_String);
@@ -2419,6 +2420,194 @@ public int Native_SBBanPlayer(Handle plugin, int numParams)
24192420
return true;
24202421
}
24212422

2423+
public int Native_SBPP_BanPlayerBySteamId(Handle plugin, int numParams)
2424+
{
2425+
if (DB == INVALID_HANDLE)
2426+
{
2427+
ThrowNativeError(SP_ERROR_NATIVE, "SourceBans++ database is not available.");
2428+
return 0;
2429+
}
2430+
2431+
int admin = GetNativeCell(1);
2432+
int iTime = GetNativeCell(4);
2433+
2434+
if (admin < 0 || admin > MaxClients)
2435+
{
2436+
ThrowNativeError(SP_ERROR_NATIVE, "SBPP_BanPlayerBySteamId: iAdmin must be 0 or a valid client index.");
2437+
return 0;
2438+
}
2439+
2440+
if (iTime < 0)
2441+
{
2442+
ThrowNativeError(SP_ERROR_NATIVE, "SBPP_BanPlayerBySteamId: iTime cannot be negative.");
2443+
return 0;
2444+
}
2445+
2446+
if (admin != 0)
2447+
{
2448+
if (!IsClientInGame(admin))
2449+
{
2450+
ThrowNativeError(SP_ERROR_NATIVE, "Ban Error: Player is not in game.");
2451+
return 0;
2452+
}
2453+
2454+
AdminId aid = GetUserAdmin(admin);
2455+
if (aid == INVALID_ADMIN_ID)
2456+
{
2457+
ThrowNativeError(SP_ERROR_NATIVE, "Ban Error: Player is not an admin.");
2458+
return 0;
2459+
}
2460+
2461+
if (!aid.HasFlag(Admin_Ban))
2462+
{
2463+
ThrowNativeError(SP_ERROR_NATIVE, "Ban Error: Player does not have BAN flag.");
2464+
return 0;
2465+
}
2466+
}
2467+
2468+
char steamId[MAX_AUTHID_LENGTH], name[MAX_NAME_LENGTH], reason[128];
2469+
GetNativeString(2, steamId, sizeof(steamId));
2470+
GetNativeString(3, name, sizeof(name));
2471+
GetNativeString(5, reason, sizeof(reason));
2472+
2473+
char validatedSteam3[MAX_AUTHID_LENGTH];
2474+
if (!Steam2ToSteam3(steamId, validatedSteam3, sizeof(validatedSteam3)))
2475+
{
2476+
ThrowNativeError(SP_ERROR_NATIVE, "SBPP_BanPlayerBySteamId: steamId must be in SteamID2 format (STEAM_[01]:[01]:Z), got: %s", steamId);
2477+
return 0;
2478+
}
2479+
2480+
if (reason[0] == '\0')
2481+
strcopy(reason, sizeof(reason), "Banned by SourceBans");
2482+
2483+
char adminAuth[MAX_AUTHID_LENGTH], adminIp[16];
2484+
if (admin == 0)
2485+
{
2486+
strcopy(adminAuth, sizeof(adminAuth), "STEAM_ID_SERVER");
2487+
strcopy(adminIp, sizeof(adminIp), ServerIp);
2488+
}
2489+
else
2490+
{
2491+
strcopy(adminAuth, sizeof(adminAuth), g_sSteamIDs[admin]);
2492+
strcopy(adminIp, sizeof(adminIp), g_sPlayerIP[admin]);
2493+
}
2494+
2495+
DataPack pack = new DataPack();
2496+
pack.WriteCell(admin == 0 ? 0 : GetClientUserId(admin));
2497+
pack.WriteCell(iTime);
2498+
pack.WriteString(reason);
2499+
pack.WriteString(steamId);
2500+
pack.WriteString(name);
2501+
pack.WriteString(adminAuth);
2502+
pack.WriteString(adminIp);
2503+
2504+
char steamIdEscaped[MAX_AUTHID_LENGTH * 2 + 1], steamIdYzEscaped[MAX_AUTHID_LENGTH * 2 + 1];
2505+
DB.Escape(steamId, steamIdEscaped, sizeof(steamIdEscaped));
2506+
DB.Escape(steamId[8], steamIdYzEscaped, sizeof(steamIdYzEscaped));
2507+
2508+
char query[512];
2509+
FormatEx(query, sizeof(query), "SELECT bid FROM %s_bans WHERE type = 0 AND (authid = '%s' OR authid REGEXP '^STEAM_[0-9]:%s$') AND (length = 0 OR ends > UNIX_TIMESTAMP()) AND RemoveType IS NULL",
2510+
DatabasePrefix, steamIdEscaped, steamIdYzEscaped);
2511+
2512+
DB.Query(DB_OnBanBySteamIdSelect, query, pack, DBPrio_High);
2513+
2514+
return 0;
2515+
}
2516+
2517+
void DB_OnBanBySteamIdSelect(Database db, DBResultSet results, const char[] error, DataPack pack)
2518+
{
2519+
if (results == null)
2520+
{
2521+
LogToFile(logFile, "[SBPP] BanPlayerBySteamId select failed: %s", error);
2522+
delete pack;
2523+
return;
2524+
}
2525+
2526+
pack.Reset();
2527+
int adminUserId = pack.ReadCell();
2528+
int iTime = pack.ReadCell();
2529+
char reason[128], steamId[MAX_AUTHID_LENGTH], name[MAX_NAME_LENGTH], adminAuth[MAX_AUTHID_LENGTH], adminIp[16];
2530+
pack.ReadString(reason, sizeof(reason));
2531+
pack.ReadString(steamId, sizeof(steamId));
2532+
pack.ReadString(name, sizeof(name));
2533+
pack.ReadString(adminAuth, sizeof(adminAuth));
2534+
pack.ReadString(adminIp, sizeof(adminIp));
2535+
delete pack;
2536+
2537+
if (results.RowCount > 0)
2538+
{
2539+
LogToFile(logFile, "[SBPP] BanPlayerBySteamId: %s is already banned, skipping.", steamId);
2540+
return;
2541+
}
2542+
2543+
char steamIdEscaped[MAX_AUTHID_LENGTH * 2 + 1], nameEscaped[MAX_NAME_LENGTH * 2 + 1], reasonEscaped[256],
2544+
adminAuthEscaped[MAX_AUTHID_LENGTH * 2 + 1], adminAuthYzEscaped[MAX_AUTHID_LENGTH * 2 + 1], adminIpEscaped[33];
2545+
if (!db.Escape(steamId, steamIdEscaped, sizeof(steamIdEscaped))
2546+
|| !db.Escape(name, nameEscaped, sizeof(nameEscaped))
2547+
|| !db.Escape(reason, reasonEscaped, sizeof(reasonEscaped))
2548+
|| !db.Escape(adminAuth, adminAuthEscaped, sizeof(adminAuthEscaped))
2549+
|| !db.Escape(adminAuth[8], adminAuthYzEscaped, sizeof(adminAuthYzEscaped))
2550+
|| !db.Escape(adminIp, adminIpEscaped, sizeof(adminIpEscaped)))
2551+
{
2552+
LogToFile(logFile, "[SBPP] BanPlayerBySteamId failed to escape ban data for %s", steamId);
2553+
return;
2554+
}
2555+
2556+
char query[1024];
2557+
if (serverID == -1)
2558+
{
2559+
FormatEx(query, sizeof(query), "INSERT INTO %s_bans (authid, name, created, ends, length, reason, aid, adminIp, sid, country) VALUES \
2560+
('%s', '%s', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + %d, %d, '%s', \
2561+
IFNULL((SELECT aid FROM %s_admins WHERE authid = '%s' OR authid REGEXP '^STEAM_[0-9]:%s$'),'0'), '%s', \
2562+
(SELECT sid FROM %s_servers WHERE ip = '%s' AND port = '%s' LIMIT 0,1), ' ')",
2563+
DatabasePrefix, steamIdEscaped, nameEscaped, (iTime * 60), (iTime * 60), reasonEscaped,
2564+
DatabasePrefix, adminAuthEscaped, adminAuthYzEscaped, adminIpEscaped,
2565+
DatabasePrefix, ServerIpEscaped, ServerPort);
2566+
}
2567+
else
2568+
{
2569+
FormatEx(query, sizeof(query), "INSERT INTO %s_bans (authid, name, created, ends, length, reason, aid, adminIp, sid, country) VALUES \
2570+
('%s', '%s', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + %d, %d, '%s', \
2571+
IFNULL((SELECT aid FROM %s_admins WHERE authid = '%s' OR authid REGEXP '^STEAM_[0-9]:%s$'),'0'), '%s', \
2572+
%d, ' ')",
2573+
DatabasePrefix, steamIdEscaped, nameEscaped, (iTime * 60), (iTime * 60), reasonEscaped,
2574+
DatabasePrefix, adminAuthEscaped, adminAuthYzEscaped, adminIpEscaped,
2575+
serverID);
2576+
}
2577+
2578+
DataPack fwdPack = new DataPack();
2579+
fwdPack.WriteCell(adminUserId);
2580+
fwdPack.WriteCell(iTime);
2581+
fwdPack.WriteString(reason);
2582+
2583+
db.Query(DB_OnBanBySteamIdInsert, query, fwdPack, DBPrio_High);
2584+
}
2585+
2586+
void DB_OnBanBySteamIdInsert(Database db, DBResultSet results, const char[] error, DataPack pack)
2587+
{
2588+
pack.Reset();
2589+
int adminUserId = pack.ReadCell();
2590+
int iTime = pack.ReadCell();
2591+
char reason[128];
2592+
pack.ReadString(reason, sizeof(reason));
2593+
delete pack;
2594+
2595+
if (results == null)
2596+
{
2597+
LogToFile(logFile, "[SBPP] BanPlayerBySteamId insert failed: %s", error);
2598+
return;
2599+
}
2600+
2601+
int admin = adminUserId == 0 ? 0 : GetClientOfUserId(adminUserId);
2602+
2603+
Call_StartForward(g_hFwd_OnBanAdded);
2604+
Call_PushCell(admin);
2605+
Call_PushCell(-1);
2606+
Call_PushCell(iTime);
2607+
Call_PushString(reason);
2608+
Call_Finish();
2609+
}
2610+
24222611
public int Native_SBReportPlayer(Handle plugin, int numParams)
24232612
{
24242613
if (numParams < 3)

‎game/addons/sourcemod/scripting/scripts/resolve-plugin-version.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ fi
4848
# Native API epoch — NOT panel semver minor. Bump MINOR_REVISION manually when
4949
# shipping additive natives; bump MAJOR only on breaking native contract.
5050
api_major=2
51-
api_minor=0
51+
api_minor=1
5252

5353
sb_major="dev"
5454
sb_minor="0"

‎web/tests/integration/PluginVersionResolveTest.php‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public function testReleaseVersionWritesTagAndApiEpoch(): void
8686
self::assertIsString($inc);
8787
self::assertStringContainsString('#define SB_VERSION "2.0.0"', $inc);
8888
self::assertStringContainsString('#define MAJOR_REVISION 2', $inc);
89-
self::assertStringContainsString('#define MINOR_REVISION 0', $inc);
89+
self::assertStringContainsString('#define MINOR_REVISION 1', $inc);
9090

9191
@rmdir($tmp);
9292
}
@@ -106,8 +106,8 @@ public function testReleaseSemverMinorDoesNotBumpApiMinorRevision(): void
106106
self::assertIsString($inc);
107107
self::assertStringContainsString('#define SB_VERSION "2.1.0"', $inc);
108108
self::assertStringContainsString('#define MAJOR_REVISION 2', $inc);
109-
self::assertStringContainsString('#define MINOR_REVISION 0', $inc);
110-
self::assertStringNotContainsString('#define MINOR_REVISION 1', $inc);
109+
self::assertStringContainsString('#define MINOR_REVISION 1', $inc);
110+
self::assertStringNotContainsString('#define MINOR_REVISION 2', $inc);
111111
}
112112

113113
public function testVersionJsonTierWhenPresent(): void
@@ -149,7 +149,7 @@ public function testVersionJsonTierWritesToIsolatedOutputPath(): void
149149
self::assertIsString($inc);
150150
self::assertStringContainsString('#define SB_VERSION "9.8.7"', $inc);
151151
self::assertStringContainsString('#define MAJOR_REVISION 2', $inc);
152-
self::assertStringContainsString('#define MINOR_REVISION 0', $inc);
152+
self::assertStringContainsString('#define MINOR_REVISION 1', $inc);
153153
self::assertSame(self::$incBackup, file_get_contents(self::incPath()));
154154

155155
@unlink($jsonPath);

0 commit comments

Comments
 (0)