/* * CSB Gag * Copyright (C) 2026 counter-strike-boost.com * * Timed chat/radio gags that survive a reconnect. Gags are held in a Trie keyed * by SteamID for O(1) lookups and mirrored into nVault so a rage-quit does not * clear the punishment. * * Inspired by the well-known "Gag/Ungag" plugin by ConnorMcLeod. This is an * independent GPL re-implementation; no original code is reused. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. It is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See for details. */ #include #include #include new const PLUGIN[] = "CSB Gag" new const VERSION[] = "1.0.0" new const AUTHOR[] = "counter-strike-boost.com" #define NO_VAULT (-1) new Trie:g_tGags new g_iVault = NO_VAULT new g_iGagUntil[33] /* unix timestamp, 0 = not gagged */ new g_szGagReason[33][64] new g_szAuth[33][35] new g_pEnabled, g_pBlockRadio, g_pMaxMinutes new const g_szBlocked[][] = { "say", "say_team", "radio1", "radio2", "radio3" } public plugin_init() { register_plugin(PLUGIN, VERSION, AUTHOR) g_pEnabled = register_cvar("csb_gag_enabled", "1") g_pBlockRadio = register_cvar("csb_gag_radio", "1") g_pMaxMinutes = register_cvar("csb_gag_max", "1440") register_concmd("amx_gag", "cmdGag", ADMIN_KICK, " [reason]") register_concmd("amx_ungag", "cmdUngag", ADMIN_KICK, "") register_concmd("amx_gaglist", "cmdGagList", ADMIN_KICK, "- lists the gagged players currently on the server") g_tGags = TrieCreate() } public plugin_cfg() { g_iVault = nvault_open("csb_gag") if (g_iVault == NO_VAULT) log_amx("[CSB Gag] could not open the nVault file 'csb_gag' - gags will not persist.") } public plugin_end() { if (g_iVault != NO_VAULT) nvault_close(g_iVault) TrieDestroy(g_tGags) } /* ---------- state ---------- */ public client_authorized(id) { g_iGagUntil[id] = 0 g_szGagReason[id][0] = 0 g_szAuth[id][0] = 0 if (is_user_bot(id) || is_user_hltv(id)) return get_user_authid(id, g_szAuth[id], charsmax(g_szAuth[])) new expire = 0 if (!TrieGetCell(g_tGags, g_szAuth[id], expire) && g_iVault != NO_VAULT) { new value[16] if (nvault_get(g_iVault, g_szAuth[id], value, charsmax(value))) expire = str_to_num(value) } /* 0 stored in the trie means "until map change", which is still active */ if (expire == -1) { g_iGagUntil[id] = -1 return } if (expire > get_systime()) { g_iGagUntil[id] = expire TrieSetCell(g_tGags, g_szAuth[id], expire) } else if (expire > 0) { clearGag(g_szAuth[id]) } } public client_disconnected(id) { g_iGagUntil[id] = 0 g_szGagReason[id][0] = 0 g_szAuth[id][0] = 0 } /* ---------- blocking ---------- */ public client_command(id) { if (!get_pcvar_num(g_pEnabled) || !isGagged(id)) return PLUGIN_CONTINUE new cmd[24] read_argv(0, cmd, charsmax(cmd)) new blockRadio = get_pcvar_num(g_pBlockRadio) for (new i = 0; i < sizeof(g_szBlocked); i++) { if (!equali(cmd, g_szBlocked[i])) continue if (i >= 2 && !blockRadio) return PLUGIN_CONTINUE if (i < 2) { new left[32] if (g_iGagUntil[id] == -1) copy(left, charsmax(left), "until the map changes") else formatex(left, charsmax(left), "for %d more minute(s)", ((g_iGagUntil[id] - get_systime()) / 60) + 1) client_print(id, print_chat, "[CSB] You are gagged %s. Reason: %s", left, g_szGagReason[id][0] ? g_szGagReason[id] : "no reason given") } return PLUGIN_HANDLED_MAIN } return PLUGIN_CONTINUE } bool:isGagged(id) { if (!g_iGagUntil[id]) return false if (g_iGagUntil[id] == -1) return true if (g_iGagUntil[id] <= get_systime()) { clearGag(g_szAuth[id]) g_iGagUntil[id] = 0 client_print(id, print_chat, "[CSB] Your gag has expired, you can talk again.") return false } return true } /* ---------- commands ---------- */ public cmdGag(id, level, cid) { if (!cmd_access(id, level, cid, 2)) return PLUGIN_HANDLED new arg[32], minsArg[8], reason[64] read_argv(1, arg, charsmax(arg)) read_argv(2, minsArg, charsmax(minsArg)) read_argv(3, reason, charsmax(reason)) new target = cmd_target(id, arg, CMDTARGET_OBEY_IMMUNITY) if (!target) return PLUGIN_HANDLED new mins = str_to_num(minsArg) new maxMins = get_pcvar_num(g_pMaxMinutes) if (mins < 0) mins = 0 if (maxMins > 0 && mins > maxMins) mins = maxMins if (!reason[0]) copy(reason, charsmax(reason), "no reason given") new expire = mins > 0 ? (get_systime() + mins * 60) : -1 g_iGagUntil[target] = expire copy(g_szGagReason[target], charsmax(g_szGagReason[]), reason) if (g_szAuth[target][0]) { TrieSetCell(g_tGags, g_szAuth[target], expire) if (g_iVault != NO_VAULT && expire > 0) { new value[16] num_to_str(expire, value, charsmax(value)) nvault_pset(g_iVault, g_szAuth[target], value) } } new aname[32], tname[32], msg[192] getAdminName(id, aname, charsmax(aname)) get_user_name(target, tname, charsmax(tname)) if (mins > 0) formatex(msg, charsmax(msg), "^x04[CSB]^x03 %s^x01 gagged^x03 %s^x01 for^x04 %d^x01 minute(s) (%s)", aname, tname, mins, reason) else formatex(msg, charsmax(msg), "^x04[CSB]^x03 %s^x01 gagged^x03 %s^x01 until the map changes (%s)", aname, tname, reason) chatAll(msg) log_amx("[CSB Gag] %s gagged %s (%s) for %d min: %s", aname, tname, g_szAuth[target], mins, reason) return PLUGIN_HANDLED } public cmdUngag(id, level, cid) { if (!cmd_access(id, level, cid, 2)) return PLUGIN_HANDLED new arg[32] read_argv(1, arg, charsmax(arg)) new target = cmd_target(id, arg, 0) if (!target) return PLUGIN_HANDLED if (!g_iGagUntil[target]) { console_print(id, "[CSB] That player is not gagged.") return PLUGIN_HANDLED } g_iGagUntil[target] = 0 g_szGagReason[target][0] = 0 clearGag(g_szAuth[target]) new aname[32], tname[32], msg[192] getAdminName(id, aname, charsmax(aname)) get_user_name(target, tname, charsmax(tname)) formatex(msg, charsmax(msg), "^x04[CSB]^x03 %s^x01 ungagged^x03 %s^x01.", aname, tname) chatAll(msg) log_amx("[CSB Gag] %s ungagged %s", aname, tname) return PLUGIN_HANDLED } public cmdGagList(id, level, cid) { if (!cmd_access(id, level, cid, 1)) return PLUGIN_HANDLED new players[32], num, pid, name[32], left[32], count = 0 get_players(players, num, "ch") console_print(id, "[CSB] Gagged players:") for (new i = 0; i < num; i++) { pid = players[i] if (!g_iGagUntil[pid]) continue get_user_name(pid, name, charsmax(name)) if (g_iGagUntil[pid] == -1) copy(left, charsmax(left), "map change") else formatex(left, charsmax(left), "%d min", ((g_iGagUntil[pid] - get_systime()) / 60) + 1) console_print(id, " %-24s %-22s %-12s %s", name, g_szAuth[pid], left, g_szGagReason[pid]) count++ } if (!count) console_print(id, " (none)") return PLUGIN_HANDLED } /* ---------- helpers ---------- */ clearGag(const authid[]) { if (!authid[0]) return if (TrieKeyExists(g_tGags, authid)) TrieDeleteKey(g_tGags, authid) if (g_iVault != NO_VAULT) nvault_remove(g_iVault, authid) } getAdminName(id, name[], len) { if (id) get_user_name(id, name, len) else copy(name, len, "CONSOLE") } chatAll(const msg[]) { new players[32], num, msgid = get_user_msgid("SayText") get_players(players, num, "ch") for (new i = 0; i < num; i++) { message_begin(MSG_ONE, msgid, _, players[i]) write_byte(players[i]) write_string(msg) message_end() } }