#include #include #pragma semicolon 1 #pragma newdecls required // So commands fail with a message instead of an unbound-native error while the core is unloaded. bool g_bCoreLoaded; // Account ids banned this map. This is the ban itself, so leaving and rejoining does not clear it. ArrayList g_alBannedAccounts; // Slot mirror. Also carries bans of unauthenticated clients (account id 0), which last the session only. bool g_bBanned[MAXPLAYERS + 1]; public Plugin myinfo = { name = "Couch Ban", author = "marisakirisame.net", description = "Bans players from the ZE couch until the end of the map", version = "2.0", url = "https://marisakirisame.net" }; // --------------------------------------------------------------------------------------------- // Plugin lifecycle // --------------------------------------------------------------------------------------------- public void OnPluginStart() { LoadTranslations("common.phrases"); // Target errors and multi-target names ("all players"). g_alBannedAccounts = new ArrayList(); RegAdminCmd("sm_couchban", Command_CouchBan, ADMFLAG_BAN, "sm_couchban <#userid|name|@filter> - bans players from the couch until the end of the map"); RegAdminCmd("sm_cban", Command_CouchBan, ADMFLAG_BAN, "Alias of sm_couchban"); RegAdminCmd("sm_couchunban", Command_CouchUnban, ADMFLAG_BAN, "sm_couchunban <#userid|name|@filter> - lifts couch bans"); RegAdminCmd("sm_cunban", Command_CouchUnban, ADMFLAG_BAN, "Alias of sm_couchunban"); } public void OnAllPluginsLoaded() { g_bCoreLoaded = LibraryExists("Couch"); // After a reload of this plugin the core still knows who is blocked. if (!g_bCoreLoaded) return; for (int i = 1; i <= MaxClients; i++) { if (!IsClientInGame(i) || !Couch_IsClientBlocked(i)) continue; g_bBanned[i] = true; RememberAccount(i); } } public void OnLibraryAdded(const char[] name) { if (!StrEqual(name, "Couch")) return; g_bCoreLoaded = true; // A core reload wiped its flags. A frame later so its natives are bound. RequestFrame(Frame_ReapplyBans); } public void OnLibraryRemoved(const char[] name) { if (StrEqual(name, "Couch")) g_bCoreLoaded = false; } void Frame_ReapplyBans(any data) { if (!g_bCoreLoaded) return; for (int i = 1; i <= MaxClients; i++) { if (g_bBanned[i] && IsClientInGame(i)) Couch_SetClientBlocked(i, true); } } public void OnMapStart() { for (int i = 1; i <= MAXPLAYERS; i++) g_bBanned[i] = false; } public void OnMapEnd() { g_alBannedAccounts.Clear(); } // The account id stays on the list so a rejoin picks the ban back up. public void OnClientDisconnect(int client) { g_bBanned[client] = false; } // Either "in game" or "authorized" can come last, so both are hooked. public void OnClientPutInServer(int client) { TryReapplyBan(client); } public void OnClientAuthorized(int client, const char[] auth) { TryReapplyBan(client); } void TryReapplyBan(int client) { if (g_bBanned[client] || !IsClientInGame(client)) return; int account = GetSteamAccountID(client); if (account == 0 || g_alBannedAccounts.FindValue(account) == -1) return; g_bBanned[client] = true; if (g_bCoreLoaded) Couch_SetClientBlocked(client, true); PrintToChat(client, PREFIX ... "You are couch-banned\x01 until the end of the map."); } // --------------------------------------------------------------------------------------------- // Commands // --------------------------------------------------------------------------------------------- public Action Command_CouchBan(int client, int args) { if (!RequireCore(client)) return Plugin_Handled; if (args < 1) { ReplyToCommand(client, PREFIX_PLAIN ... " Usage: sm_couchban <#userid|name|@filter>"); return Plugin_Handled; } ProcessBanCommand(client, true); return Plugin_Handled; } public Action Command_CouchUnban(int client, int args) { if (!RequireCore(client)) return Plugin_Handled; if (args < 1) { ReplyToCommand(client, PREFIX_PLAIN ... " Usage: sm_couchunban <#userid|name|@filter>"); return Plugin_Handled; } ProcessBanCommand(client, false); return Plugin_Handled; } // Standard SourceMod targeting: a name, #userid or @filter (@all, @t, @zombies ...). Players already in // the wanted state are skipped. One chat line covers the whole command; each ban is still logged. void ProcessBanCommand(int client, bool bBan) { char sArg[MAX_TARGET_LENGTH]; GetCmdArg(1, sArg, sizeof(sArg)); int targets[MAXPLAYERS]; char sTargetName[MAX_TARGET_LENGTH]; bool bIsML; int count = ProcessTargetString(sArg, client, targets, sizeof(targets), COMMAND_FILTER_CONNECTED | COMMAND_FILTER_NO_BOTS, sTargetName, sizeof(sTargetName), bIsML); if (count <= 0) { ReplyToTargetError(client, count); return; } int affected = 0; int last = 0; for (int i = 0; i < count; i++) { int target = targets[i]; if (!IsClientInGame(target) || g_bBanned[target] == bBan) continue; if (bBan) DoBan(client, target); else DoUnban(client, target); affected++; last = target; } if (affected == 0) { if (count == 1) ReplyToCommand(client, PREFIX_PLAIN ... " %N is %s couch-banned.", targets[0], bBan ? "already" : "not"); else ReplyToCommand(client, PREFIX_PLAIN ... " No matching players to couch-%s.", bBan ? "ban" : "unban"); return; } // Announced to everyone so players know why the couch stopped reacting to someone. A single // player is named even when they were picked out of a filter. if (affected == 1) { if (bBan) ShowActivity2(client, PREFIX, "\x01couch-banned \x04%N\x01 until the end of the map.", last); else ShowActivity2(client, PREFIX, "\x01removed couch-ban from \x04%N\x01.", last); } else if (bIsML) { if (bBan) ShowActivity2(client, PREFIX, "\x01couch-banned \x04%t\x01 until the end of the map.", sTargetName); else ShowActivity2(client, PREFIX, "\x01removed couch-ban from \x04%t\x01.", sTargetName); } else { if (bBan) ShowActivity2(client, PREFIX, "\x01couch-banned \x04%s\x01 until the end of the map.", sTargetName); else ShowActivity2(client, PREFIX, "\x01removed couch-ban from \x04%s\x01.", sTargetName); } } bool RequireCore(int client) { if (g_bCoreLoaded) return true; ReplyToCommand(client, PREFIX_PLAIN ... " The Couch core plugin is not loaded."); return false; } // --------------------------------------------------------------------------------------------- // Actions // --------------------------------------------------------------------------------------------- // The announcement is the command's job, so a filter doesn't produce a line per player. void DoBan(int client, int target) { g_bBanned[target] = true; RememberAccount(target); Couch_SetClientBlocked(target, true); LogAction(client, target, "\"%L\" couch-banned \"%L\" until the end of the map.", client, target); } void DoUnban(int client, int target) { g_bBanned[target] = false; ForgetAccount(target); Couch_SetClientBlocked(target, false); LogAction(client, target, "\"%L\" removed couch-ban from \"%L\".", client, target); } // An unauthenticated client has no account id, so their ban ends with the session. void RememberAccount(int client) { int account = GetSteamAccountID(client); if (account != 0 && g_alBannedAccounts.FindValue(account) == -1) g_alBannedAccounts.Push(account); } void ForgetAccount(int client) { int account = GetSteamAccountID(client); if (account == 0) return; int index = g_alBannedAccounts.FindValue(account); if (index != -1) g_alBannedAccounts.Erase(index); }