/*
* 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()
}
}