From 0a73621a89e1f6892910363326e3b34dcddade6d Mon Sep 17 00:00:00 2001 From: Rushaway Date: Fri, 13 Feb 2026 22:51:40 +0100 Subject: [PATCH 1/5] refactor(v3): code cleanup, rename plugin --- .github/workflows/ci.yml | 1 + addons/sourcemod/scripting/ButtonNotifier.sp | 365 --------------- addons/sourcemod/scripting/TriggerWatcher.sp | 440 ++++++++++++++++++ .../translations/TriggerWatcher.phrases.txt | 164 +++++++ sourceknight.yaml | 4 +- 5 files changed, 607 insertions(+), 367 deletions(-) delete mode 100644 addons/sourcemod/scripting/ButtonNotifier.sp create mode 100644 addons/sourcemod/scripting/TriggerWatcher.sp create mode 100644 addons/sourcemod/translations/TriggerWatcher.phrases.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c09fe8..0ae93fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: run: | mkdir -p /tmp/package cp -R .sourceknight/package/* /tmp/package + cp -R addons/sourcemod/translations /tmp/package/common/addons/sourcemod/ - name: Upload build archive for test runners uses: actions/upload-artifact@v6 diff --git a/addons/sourcemod/scripting/ButtonNotifier.sp b/addons/sourcemod/scripting/ButtonNotifier.sp deleted file mode 100644 index bca4091..0000000 --- a/addons/sourcemod/scripting/ButtonNotifier.sp +++ /dev/null @@ -1,365 +0,0 @@ -#pragma semicolon 1 - -#include -#include -#include -#include -#include -#undef REQUIRE_PLUGIN -#tryinclude -#define REQUIRE_PLUGIN - -#pragma newdecls required - -#define CONSOLE 0 -#define CHAT 1 - -bool g_bLate = false; - -ConVar g_cBlockSpam; -ConVar g_cBlockSpamDelay; - -Handle g_hPreferences = INVALID_HANDLE; - -int g_iButtonsDisplay[MAXPLAYERS+1]; -int g_iTriggersDisplay[MAXPLAYERS+1]; -int g_ilastButtonUse[MAXPLAYERS+1] = { -1, ... }; -int g_iwaitBeforeButtonUse[MAXPLAYERS+1] = { -1, ... }; - -bool g_bTriggered[2048] = { false, ... }; - -public Plugin myinfo = -{ - name = "Button & Triggers Notifier", - author = "Silence, maxime1907, .Rushaway", - description = "Logs button and trigger presses to the chat.", - version = "2.1.3", - url = "" -}; - -public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) -{ - g_bLate = late; - return APLRes_Success; -} - -public void OnPluginStart() -{ - /* CONVARS */ - g_cBlockSpam = CreateConVar("sm_buttonnotifier_block_spam", "1", "Blocks spammers abusing certain buttons", FCVAR_NONE, true, 0.0, true, 1.0); - g_cBlockSpamDelay = CreateConVar("sm_buttonnotifier_block_spam_delay", "5", "Time to wait before notifying the next button press", FCVAR_NONE, true, 1.0, true, 60.0); - - AutoExecConfig(true); - - /* COOKIES */ - SetCookieMenuItem(CookieHandler, 0, "Buttons Notifier Settings"); - g_hPreferences = RegClientCookie("bn_preferences", "Button and Trigger notification preferences", CookieAccess_Protected); - - /* HOOKS */ - HookEvent("round_start", Event_RoundStart, EventHookMode_Pre); - - if (!g_bLate) - return; - - for (int i = 1; i < MaxClients; i++) - { - if (!IsClientConnected(i) || IsFakeClient(i) || !AreClientCookiesCached(i)) - continue; - - ReadClientCookies(i); - } - - g_bLate = false; -} - -public void OnMapStart() -{ - CreateTimer(1.0, Timer_HookButtons, _, TIMER_FLAG_NO_MAPCHANGE); -} - -public void Event_RoundStart(Event hEvent, const char[] sName, bool bDontBroadcast) -{ - // Reset values - for (int i = 1; i <= 2047; i++) - { - g_bTriggered[i] = false; - } - - for (int i = 1; i <= MaxClients; i++) - { - g_ilastButtonUse[i] = -1; - g_iwaitBeforeButtonUse[i] = -1; - } -} - -public void OnMapEnd() -{ - UnhookEntityOutput("func_button", "OnPressed", ButtonPressed); - UnhookEntityOutput("trigger_once", "OnTrigger", TriggerTouched); - UnhookEntityOutput("trigger_multiple", "OnStartTouch", TriggerTouched); - UnhookEntityOutput("trigger_teleport", "OnStartTouch", TriggerTouched); -} - -public void OnClientCookiesCached(int client) -{ - ReadClientCookies(client); -} - -public void ReadClientCookies(int client) -{ - char sValue[32]; - GetClientCookie(client, g_hPreferences, sValue, 32); - - if (strlen(sValue) >= 2) - { - char sTemp[2]; - FormatEx(sTemp, sizeof(sTemp), "%c", sValue[0]); - g_iButtonsDisplay[client] = StringToInt(sTemp); - - FormatEx(sTemp, sizeof(sTemp), "%c", sValue[1]); - g_iTriggersDisplay[client] = StringToInt(sTemp); - } - else - { - // Set default values if no cookie exists or invalid format - g_iButtonsDisplay[client] = CONSOLE; - g_iTriggersDisplay[client] = CONSOLE; - } -} - -public void SetClientCookies(int client) -{ - char sValue[8]; - FormatEx(sValue, sizeof(sValue), "%d%d", g_iButtonsDisplay[client], g_iTriggersDisplay[client]); - SetClientCookie(client, g_hPreferences, sValue); -} - -public void CookieHandler(int client, CookieMenuAction action, any info, char[] buffer, int maxlen) -{ - switch (action) - { - case CookieMenuAction_SelectOption: - { - NotifierSetting(client); - } - } -} - -public void NotifierSetting(int client) -{ - Menu menu = new Menu(NotifierSettingHandler, MENU_ACTIONS_ALL); - - menu.SetTitle("Buttons - Triggers Notifier Settings"); - - char buttons[64], triggers[64]; - FormatEx(buttons, 64, "Buttons"); - FormatEx(triggers, 64, "Triggers"); - - menu.AddItem("buttons", buttons); - menu.AddItem("triggers", triggers); - - menu.ExitBackButton = true; - menu.ExitButton = true; - menu.Display(client, MENU_TIME_FOREVER); -} - -public int NotifierSettingHandler(Menu menu, MenuAction action, int param1, int param2) -{ - switch (action) - { - case MenuAction_DisplayItem: - { - char type[32], info[64], display[64]; - menu.GetItem(param2, info, sizeof(info)); - if (strcmp(info, "buttons", false) == 0) - { - if (g_iButtonsDisplay[param1] == CONSOLE) - FormatEx(type, sizeof(type), "Console"); - else - FormatEx(type, sizeof(type), "Chat"); - - FormatEx(display, sizeof(display), "Buttons: %s", type); - return RedrawMenuItem(display); - } - else if (strcmp(info, "triggers", false) == 0) - { - if (g_iTriggersDisplay[param1] == CONSOLE) - FormatEx(type, sizeof(type), "Console"); - else - FormatEx(type, sizeof(type), "Chat"); - - FormatEx(display, sizeof(display), "Triggers: %s", type); - return RedrawMenuItem(display); - } - } - case MenuAction_Select: - { - char info[64]; - menu.GetItem(param2, info, sizeof(info)); - if (strcmp(info, "buttons", false) == 0) - { - if (g_iButtonsDisplay[param1] == CONSOLE) - { - g_iButtonsDisplay[param1] = CHAT; - CPrintToChat(param1, "{red}[Button Notifier] {lightgreen}You set display in {blue}Chat"); - } - else - { - g_iButtonsDisplay[param1] = CONSOLE; - CPrintToChat(param1, "{red}[Button Notifier] {lightgreen}You set display in {blue}Console"); - } - } - else if (strcmp(info, "triggers", false) == 0) - { - if (g_iTriggersDisplay[param1] == CONSOLE) - { - g_iTriggersDisplay[param1] = CHAT; - CPrintToChat(param1, "{red}[Trigger Notifier] {lightgreen}You set display in {blue}Chat"); - } - else - { - g_iTriggersDisplay[param1] = CONSOLE; - CPrintToChat(param1, "{red}[Trigger Notifier] {lightgreen}You set display in {blue}Console"); - } - } - - SetClientCookies(param1); - NotifierSetting(param1); - } - case MenuAction_Cancel: - { - ShowCookieMenu(param1); - } - case MenuAction_End: - { - delete menu; - } - } - return 0; -} - -public Action Timer_HookButtons(Handle timer) -{ - HookEntityOutput("func_button", "OnPressed", ButtonPressed); - HookEntityOutput("trigger_once", "OnTrigger", TriggerTouched); - HookEntityOutput("trigger_multiple", "OnStartTouch", TriggerTouched); - HookEntityOutput("trigger_teleport", "OnStartTouch", TriggerTouched); - return Plugin_Stop; -} - -public void TriggerTouched(const char[] output, int caller, int activator, float delay) -{ - if (g_bTriggered[caller] || !IsValidClient(activator)) - return; - - g_bTriggered[caller] = true; - - char sClassname[32]; - GetEdictClassname(caller, sClassname, sizeof(sClassname)); - ReplaceString(sClassname, sizeof(sClassname), "trigger_", "", false); - - char entity[64]; - GetEntPropString(caller, Prop_Data, "m_iName", entity, sizeof(entity)); - - if (strcmp(entity, "", false) == 0) - FormatEx(entity, sizeof(entity), "trigger #%d", caller); - - char userid[64]; - GetClientAuthId(activator, AuthId_Steam3, userid, sizeof(userid), false); - ReplaceString(userid, sizeof(userid), "[", "", true); - ReplaceString(userid, sizeof(userid), "]", "", true); - Format(userid, sizeof(userid), "#%d|%s", GetClientUserId(activator), userid); - - for (int i = 1; i <= MaxClients; i++) - { - if(IsClientConnected(i) && IsClientInGame(i) && (IsClientSourceTV(i) || GetAdminFlag(GetUserAdmin(i), Admin_Generic))) - { - if (g_iTriggersDisplay[i] == CONSOLE) - PrintToConsole(i, "[Notifier - Trigger %s] %N (%s) triggered %s", sClassname, activator, userid, entity); - else - CPrintToChat(i, "{red}[Notifier - Trigger %s] {white}%N {red}({grey}%s{red}) {lightgreen}triggered {blue}%s", sClassname, activator, userid, entity); - } - } - - PrintToServer("[Trigger Notifier] %N (%s) triggered %s", activator, userid, entity); -} - -public void ButtonPressed(const char[] output, int caller, int activator, float delay) -{ - if (!IsValidClient(activator) || !IsValidEntity(caller)) - return; - -#if defined _EntWatch_include - int parent = GetEntPropEnt(caller, Prop_Data, "m_hParent"); - if (IsValidEntity(parent) && EntWatch_IsSpecialItem(parent)) - return; -#endif - int currentTime = GetTime(); - - char entity[64]; - GetEntPropString(caller, Prop_Data, "m_iName", entity, sizeof(entity)); - - if (strcmp(entity, "", false) == 0) - FormatEx(entity, sizeof(entity), "button #%d", caller); - - char userid[64]; - GetClientAuthId(activator, AuthId_Steam3, userid, sizeof(userid), false); - ReplaceString(userid, sizeof(userid), "[", "", false); - ReplaceString(userid, sizeof(userid), "]", "", false); - Format(userid, sizeof(userid), "#%d|%s", GetClientUserId(activator), userid); - ReplaceString(userid, sizeof(userid), "STEAM_", "", true); - - // activator (client) is spamming the button - if (g_cBlockSpam.BoolValue && g_ilastButtonUse[activator] != -1 && ((currentTime - g_ilastButtonUse[activator]) <= g_cBlockSpamDelay.IntValue)) - { - // if the delay time is passed, we reset the time - if (g_iwaitBeforeButtonUse[activator] != -1 && g_iwaitBeforeButtonUse[activator] <= currentTime) - { - g_iwaitBeforeButtonUse[activator] = -1; - } - - // if everything is okay send a first alert - if (g_iwaitBeforeButtonUse[activator] == -1) - { - for(int i = 1; i <= MaxClients; i++) - { - if(IsClientConnected(i) && IsClientInGame(i) && (IsClientSourceTV(i) || GetAdminFlag(GetUserAdmin(i), Admin_Generic))) - { - if (g_iTriggersDisplay[i] == CONSOLE) - PrintToConsole(i, "[Button Notifier] %N (%s) is spamming %s", activator, userid, entity); - else - CPrintToChat(i, "{red}[Button Notifier] {white}%N {red}({grey}%s{red}) {lightgreen}is spamming {blue}%s", activator, userid, entity); - } - } - - PrintToServer("[Button Notifier] %N (%s) is spamming %s", activator, userid, entity); - g_iwaitBeforeButtonUse[activator] = currentTime + g_cBlockSpamDelay.IntValue; - } - } - else - { - for(int i = 1; i <= MaxClients; i++) - { - if(IsClientConnected(i) && IsClientInGame(i) && (IsClientSourceTV(i) || GetAdminFlag(GetUserAdmin(i), Admin_Generic))) - { - if (g_iButtonsDisplay[i] == CONSOLE) - PrintToConsole(i, "[Button Notifier] %N (%s) triggered %s", activator, userid, entity); - else - CPrintToChat(i, "{red}[Button Notifier] {white}%N {red}({grey}%s{red}) {lightgreen}triggered {blue}%s", activator, userid, entity); - } - } - - PrintToServer("[Button Notifier] %N (%s) triggered %s", activator, userid, entity); - } - - g_ilastButtonUse[activator] = currentTime; -} - -bool IsValidClient(int client, bool nobots = true) -{ - if (client <= 0 || client > MaxClients || !IsClientConnected(client) || (nobots && IsFakeClient(client))) - { - return false; - } - return IsClientInGame(client); -} diff --git a/addons/sourcemod/scripting/TriggerWatcher.sp b/addons/sourcemod/scripting/TriggerWatcher.sp new file mode 100644 index 0000000..21f3e7e --- /dev/null +++ b/addons/sourcemod/scripting/TriggerWatcher.sp @@ -0,0 +1,440 @@ +#pragma semicolon 1 +#pragma newdecls required + +#include +#include +#include +#include +#include +#undef REQUIRE_PLUGIN +#tryinclude +#define REQUIRE_PLUGIN + +enum NotifyMode +{ + Notify_None = 0, + Notify_Chat, + Notify_Console, + Notify_Both +} + +#define TW_TAG "[TW]" +#define TW_ENTITY_MAX 2047 +#define TW_ENTITY_ARRAY_SIZE (TW_ENTITY_MAX + 1) + +bool g_bLate = false; + +ConVar g_hCVar_SpamDelay; + +Cookie g_hCookie_DisplayType; + +enum struct ClientState +{ + NotifyMode buttonsDisplay; + NotifyMode triggersDisplay; + int lastButtonUse; + int waitBeforeButtonUse; +} + +ClientState g_ClientState[MAXPLAYERS+1]; + +bool g_bTriggered[TW_ENTITY_ARRAY_SIZE] = { false, ... }; + +public Plugin myinfo = +{ + name = "TriggerWatcher", + author = "Silence, maxime1907, .Rushaway", + description = "Logs button and trigger presses to the chat.", + version = "3.0.0", + url = "" +}; + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + g_bLate = late; + return APLRes_Success; +} + +public void OnPluginStart() +{ + LoadTranslations("TriggerWatcher.phrases"); + + for (int i = 1; i <= MaxClients; i++) + { + g_ClientState[i].buttonsDisplay = Notify_Console; + g_ClientState[i].triggersDisplay = Notify_Console; + g_ClientState[i].lastButtonUse = -1; + g_ClientState[i].waitBeforeButtonUse = -1; + } + + /* CONVARS */ + g_hCVar_SpamDelay = CreateConVar("sm_TriggerWatcher_block_spam_delay", "5", "Time to wait before notifying the next button press", FCVAR_NONE, true, 1.0, true, 60.0); + + AutoExecConfig(true); + + /* COOKIES */ + SetCookieMenuItem(CookieHandler, 0, "TriggerWatcher Settings"); + g_hCookie_DisplayType = new Cookie("TriggerWatcher_display", "TriggerWatcher display method", CookieAccess_Private); + + /* HOOKS */ + HookEvent("round_start", Event_RoundStart, EventHookMode_Pre); + + if (!g_bLate) + return; + + for (int i = 1; i < MaxClients; i++) + { + if (!IsClientConnected(i) || IsFakeClient(i) || !AreClientCookiesCached(i)) + continue; + + ReadClientCookies(i); + } + + g_bLate = false; +} + +public void OnMapStart() +{ + CreateTimer(1.0, Timer_HookEntities, _, TIMER_FLAG_NO_MAPCHANGE); +} + +public void Event_RoundStart(Event hEvent, const char[] sName, bool bDontBroadcast) +{ + // Reset values + for (int i = 1; i <= TW_ENTITY_MAX; i++) + { + g_bTriggered[i] = false; + } + + for (int i = 1; i <= MaxClients; i++) + { + g_ClientState[i].lastButtonUse = -1; + g_ClientState[i].waitBeforeButtonUse = -1; + } +} + +public void OnMapEnd() +{ + UnhookEntityOutput("func_button", "OnPressed", ButtonPressed); + UnhookEntityOutput("trigger_once", "OnTrigger", TriggerTouched); + UnhookEntityOutput("trigger_multiple", "OnStartTouch", TriggerTouched); + UnhookEntityOutput("trigger_teleport", "OnStartTouch", TriggerTouched); +} + +public void OnClientCookiesCached(int client) +{ + ReadClientCookies(client); +} + +public void ReadClientCookies(int client) +{ + char sValue[32]; + g_hCookie_DisplayType.Get(client, sValue, sizeof(sValue)); + + if (strlen(sValue) >= 2) + { + char sTemp[2]; + FormatEx(sTemp, sizeof(sTemp), "%c", sValue[0]); + g_ClientState[client].buttonsDisplay = ClampNotifyMode(view_as(StringToInt(sTemp))); + + FormatEx(sTemp, sizeof(sTemp), "%c", sValue[1]); + g_ClientState[client].triggersDisplay = ClampNotifyMode(view_as(StringToInt(sTemp))); + } + else + { + // Set default values if no cookie exists or invalid format + g_ClientState[client].buttonsDisplay = Notify_Console; + g_ClientState[client].triggersDisplay = Notify_Console; + } +} + +public void SetClientCookies(int client) +{ + char sValue[8]; + FormatEx(sValue, sizeof(sValue), "%d%d", g_ClientState[client].buttonsDisplay, g_ClientState[client].triggersDisplay); + g_hCookie_DisplayType.Set(client, sValue); +} + +public void CookieHandler(int client, CookieMenuAction action, any info, char[] buffer, int maxlen) +{ + switch (action) + { + case CookieMenuAction_DisplayOption: + { + FormatEx(buffer, maxlen, "%T", "TW_Menu_Cookie", client); + } + case CookieMenuAction_SelectOption: + { + NotifierSetting(client); + } + } +} + +public void NotifierSetting(int client) +{ + Menu menu = new Menu(NotifierSettingHandler, MENU_ACTIONS_ALL); + + char title[128]; + FormatEx(title, sizeof(title), "%T", "TW_Menu_Title", client); + menu.SetTitle(title); + + char buttons[64], triggers[64]; + FormatEx(buttons, sizeof(buttons), "%T", "TW_Menu_Buttons", client); + FormatEx(triggers, sizeof(triggers), "%T", "TW_Menu_Triggers", client); + + menu.AddItem("buttons", buttons); + menu.AddItem("triggers", triggers); + + menu.ExitBackButton = true; + menu.ExitButton = true; + menu.Display(client, MENU_TIME_FOREVER); +} + +public int NotifierSettingHandler(Menu menu, MenuAction action, int param1, int param2) +{ + switch (action) + { + case MenuAction_DisplayItem: + { + char type[32], info[64], display[64]; + menu.GetItem(param2, info, sizeof(info)); + if (strcmp(info, "buttons", false) == 0) + { + GetNotifyModeLabel(param1, g_ClientState[param1].buttonsDisplay, type, sizeof(type)); + FormatEx(display, sizeof(display), "%T", "TW_Menu_Buttons_Display", param1, type); + return RedrawMenuItem(display); + } + else if (strcmp(info, "triggers", false) == 0) + { + GetNotifyModeLabel(param1, g_ClientState[param1].triggersDisplay, type, sizeof(type)); + FormatEx(display, sizeof(display), "%T", "TW_Menu_Triggers_Display", param1, type); + return RedrawMenuItem(display); + } + } + case MenuAction_Select: + { + char info[64]; + menu.GetItem(param2, info, sizeof(info)); + if (strcmp(info, "buttons", false) == 0) + { + g_ClientState[param1].buttonsDisplay = NextNotifyMode(g_ClientState[param1].buttonsDisplay); + NotifyModeChat(param1, g_ClientState[param1].buttonsDisplay, "TW_Menu_Buttons_Display"); + } + else if (strcmp(info, "triggers", false) == 0) + { + g_ClientState[param1].triggersDisplay = NextNotifyMode(g_ClientState[param1].triggersDisplay); + NotifyModeChat(param1, g_ClientState[param1].triggersDisplay, "TW_Menu_Triggers_Display"); + } + + SetClientCookies(param1); + NotifierSetting(param1); + } + case MenuAction_Cancel: + { + ShowCookieMenu(param1); + } + case MenuAction_End: + { + delete menu; + } + } + return 0; +} + +public Action Timer_HookEntities(Handle timer) +{ + HookEntityOutput("func_button", "OnPressed", ButtonPressed); + HookEntityOutput("func_rot_button", "OnPressed", ButtonPressed); + HookEntityOutput("trigger_once", "OnTrigger", TriggerTouched); + HookEntityOutput("trigger_multiple", "OnStartTouch", TriggerTouched); + HookEntityOutput("trigger_teleport", "OnStartTouch", TriggerTouched); + return Plugin_Stop; +} + +public void TriggerTouched(const char[] output, int caller, int activator, float delay) +{ + if (g_bTriggered[caller] || !IsValidClient(activator)) + { + return; + } + + g_bTriggered[caller] = true; + + char sClassname[32], entity[64], userid[64]; + GetTriggerClassname(caller, sClassname, sizeof(sClassname)); + GetTriggerDisplayName(caller, sClassname, entity, sizeof(entity)); + BuildUserIdString(activator, userid, sizeof(userid), true, false); + NotifyTrigger(activator, userid, entity); +} + +public void ButtonPressed(const char[] output, int caller, int activator, float delay) +{ + if (!IsValidClient(activator) || !IsValidEntity(caller)) + { + return; + } + +#if defined _EntWatch_include + int parent = GetEntPropEnt(caller, Prop_Data, "m_hParent"); + if (IsValidEntity(parent) && EntWatch_IsSpecialItem(parent)) + return; +#endif + + int currentTime = GetTime(); + + char entity[64]; + GetEntityDisplayName(caller, "button", entity, sizeof(entity)); + + char userid[64]; + BuildUserIdString(activator, userid, sizeof(userid), false, true); + + // activator (client) is spamming the button + if (g_hCVar_SpamDelay.IntValue > 0 && g_ClientState[activator].lastButtonUse != -1 && ((currentTime - g_ClientState[activator].lastButtonUse) <= g_hCVar_SpamDelay.IntValue)) + { + // if the delay time is passed, we reset the time + if (g_ClientState[activator].waitBeforeButtonUse != -1 && g_ClientState[activator].waitBeforeButtonUse <= currentTime) + { + g_ClientState[activator].waitBeforeButtonUse = -1; + } + + // if everything is okay send a first alert + if (g_ClientState[activator].waitBeforeButtonUse == -1) + { + NotifyButton(activator, userid, entity, true); + g_ClientState[activator].waitBeforeButtonUse = currentTime + g_hCVar_SpamDelay.IntValue; + } + } + else + { + NotifyButton(activator, userid, entity, false); + } + + g_ClientState[activator].lastButtonUse = currentTime; +} + +bool IsValidClient(int client, bool nobots = true) +{ + if (client <= 0 || client > MaxClients || !IsClientConnected(client) || (nobots && IsFakeClient(client))) + { + return false; + } + return IsClientInGame(client); +} + +bool ShouldNotifyClient(int client) +{ + return IsClientConnected(client) && IsClientInGame(client) && (IsClientSourceTV(client) || GetAdminFlag(GetUserAdmin(client), Admin_Generic)); +} + +void GetTriggerClassname(int caller, char[] buffer, int maxlen) +{ + GetEdictClassname(caller, buffer, maxlen); + ReplaceString(buffer, maxlen, "trigger_", "", false); +} + +void GetEntityDisplayName(int caller, const char[] fallbackPrefix, char[] buffer, int maxlen) +{ + GetEntPropString(caller, Prop_Data, "m_iName", buffer, maxlen); + if (strcmp(buffer, "", false) == 0) + FormatEx(buffer, maxlen, "%s #%d", fallbackPrefix, caller); +} + +void GetTriggerDisplayName(int caller, const char[] classname, char[] buffer, int maxlen) +{ + FormatEx(buffer, maxlen, "trigger %s #%d", classname, caller); +} + +void BuildUserIdString(int client, char[] buffer, int maxlen, bool caseSensitive, bool stripSteamPrefix) +{ + GetClientAuthId(client, AuthId_Steam3, buffer, maxlen, false); + ReplaceString(buffer, maxlen, "[", "", caseSensitive); + ReplaceString(buffer, maxlen, "]", "", caseSensitive); + Format(buffer, maxlen, "#%d|%s", GetClientUserId(client), buffer); + if (stripSteamPrefix) + ReplaceString(buffer, maxlen, "STEAM_", "", true); +} + +void NotifyTrigger(int activator, const char[] userid, const char[] entity) +{ + for (int i = 1; i <= MaxClients; i++) + { + if (!ShouldNotifyClient(i)) + continue; + + NotifyMode mode = g_ClientState[i].triggersDisplay; + if (mode == Notify_Console || mode == Notify_Both) + PrintToConsole(i, "%T", "TW_Trigger_Console", i, TW_TAG, activator, userid, entity); + if (mode == Notify_Chat || mode == Notify_Both) + CPrintToChat(i, "%t", "TW_Trigger", TW_TAG, activator, entity); + } + + PrintToServer("%T", "TW_Trigger_Console", 0, TW_TAG, activator, userid, entity); +} + +void NotifyButton(int activator, const char[] userid, const char[] entity, bool isSpam) +{ + for (int i = 1; i <= MaxClients; i++) + { + if (!ShouldNotifyClient(i)) + continue; + + NotifyMode mode = isSpam ? g_ClientState[i].triggersDisplay : g_ClientState[i].buttonsDisplay; + if (mode == Notify_Console || mode == Notify_Both) + { + if (isSpam) + PrintToConsole(i, "%T", "TW_Spamming_Console", i, TW_TAG, activator, userid, entity); + else + PrintToConsole(i, "%T", "TW_Button_Console", i, TW_TAG, activator, userid, entity); + } + if (mode == Notify_Chat || mode == Notify_Both) + { + if (isSpam) + CPrintToChat(i, "%t", "TW_Spamming", TW_TAG, activator, entity); + else + CPrintToChat(i, "%t", "TW_Button", TW_TAG, activator, entity); + } + } + + if (isSpam) + PrintToServer("%T", "TW_Spamming_Console", 0, TW_TAG, activator, userid, entity); + else + PrintToServer("%T", "TW_Button_Console", 0, TW_TAG, activator, userid, entity); +} + +NotifyMode ClampNotifyMode(NotifyMode value) +{ + if (value < Notify_None || value > Notify_Both) + return Notify_Console; + + return value; +} + +NotifyMode NextNotifyMode(NotifyMode current) +{ + current++; + if (current > Notify_Both) + current = Notify_None; + + return current; +} + +void GetNotifyModeLabel(int client, NotifyMode mode, char[] buffer, int maxlen) +{ + switch (mode) + { + case Notify_None: FormatEx(buffer, maxlen, "%T", "TW_Mode_None", client); + case Notify_Chat: FormatEx(buffer, maxlen, "%T", "TW_Mode_Chat", client); + case Notify_Console: FormatEx(buffer, maxlen, "%T", "TW_Mode_Console", client); + case Notify_Both: FormatEx(buffer, maxlen, "%T", "TW_Mode_Both", client); + default: FormatEx(buffer, maxlen, "%T", "TW_Mode_Console", client); + } +} + +void NotifyModeChat(int client, NotifyMode mode, const char[] detailPhrase) +{ + char label[16]; + GetNotifyModeLabel(client, mode, label, sizeof(label)); + char detail[64]; + FormatEx(detail, sizeof(detail), "%T", detailPhrase, client, label); + CPrintToChat(client, "{red}%s {lightgreen}%s", TW_TAG, detail); +} \ No newline at end of file diff --git a/addons/sourcemod/translations/TriggerWatcher.phrases.txt b/addons/sourcemod/translations/TriggerWatcher.phrases.txt new file mode 100644 index 0000000..1a3736b --- /dev/null +++ b/addons/sourcemod/translations/TriggerWatcher.phrases.txt @@ -0,0 +1,164 @@ +"Phrases" +{ + "TW_Trigger" + { + "#format" "{1:s},{2:N},{3:s}" + "en" "{red}{1} {white}{2} {lightgreen}triggered {blue}{3}" + "fr" "{red}{1} {white}{2} {lightgreen}a declenche {blue}{3}" + "ru" "{red}{1} {white}{2} {lightgreen}активировал {blue}{3}" + "chi" "{red}{1} {white}{2} {lightgreen}触发了 {blue}{3}" + "es" "{red}{1} {white}{2} {lightgreen}activo {blue}{3}" + } + + "TW_Button" + { + "#format" "{1:s},{2:N},{3:s}" + "en" "{red}{1} {white}{2} {lightgreen}triggered {blue}{3}" + "fr" "{red}{1} {white}{2} {lightgreen}a declenche {blue}{3}" + "ru" "{red}{1} {white}{2} {lightgreen}активировал {blue}{3}" + "chi" "{red}{1} {white}{2} {lightgreen}触发了 {blue}{3}" + "es" "{red}{1} {white}{2} {lightgreen}activo {blue}{3}" + } + + "TW_Spamming" + { + "#format" "{1:s},{2:N},{3:s}" + "en" "{red}{1} {white}{2} {lightgreen}spamming {blue}{3}" + "fr" "{red}{1} {white}{2} {lightgreen}spam {blue}{3}" + "ru" "{red}{1} {white}{2} {lightgreen}спамит {blue}{3}" + "chi" "{red}{1} {white}{2} {lightgreen}正在刷 {blue}{3}" + "es" "{red}{1} {white}{2} {lightgreen}spamea {blue}{3}" + } + + "TW_Trigger_Console" + { + "#format" "{1:s},{2:N},{3:s},{4:s}" + "en" "{1} {2} ({3}) triggered {4}" + "fr" "{1} {2} ({3}) a declenche {4}" + "ru" "{1} {2} ({3}) активировал {4}" + "chi" "{1} {2} ({3}) 触发了 {4}" + "es" "{1} {2} ({3}) activo {4}" + } + + "TW_Button_Console" + { + "#format" "{1:s},{2:N},{3:s},{4:s}" + "en" "{1} {2} ({3}) triggered {4}" + "fr" "{1} {2} ({3}) a declenche {4}" + "ru" "{1} {2} ({3}) активировал {4}" + "chi" "{1} {2} ({3}) 触发了 {4}" + "es" "{1} {2} ({3}) activo {4}" + } + + "TW_Spamming_Console" + { + "#format" "{1:s},{2:N},{3:s},{4:s}" + "en" "{1} {2} ({3}) is spamming {4}" + "fr" "{1} {2} ({3}) spam {4}" + "ru" "{1} {2} ({3}) спамит {4}" + "chi" "{1} {2} ({3}) 正在刷 {4}" + "es" "{1} {2} ({3}) spamea {4}" + } + + "TW_Display" + { + "#format" "{1:s},{2:s}" + "en" "{red}{1} {lightgreen}Display: {blue}{2}" + "fr" "{red}{1} {lightgreen}Affichage: {blue}{2}" + "ru" "{red}{1} {lightgreen}Отображение: {blue}{2}" + "chi" "{red}{1} {lightgreen}显示: {blue}{2}" + "es" "{red}{1} {lightgreen}Visualizacion: {blue}{2}" + } + + "TW_Mode_None" + { + "en" "None" + "fr" "Aucun" + "ru" "Нет" + "chi" "关闭" + "es" "Ninguno" + } + + "TW_Mode_Chat" + { + "en" "Chat" + "fr" "Chat" + "ru" "Чат" + "chi" "聊天" + "es" "Chat" + } + + "TW_Mode_Console" + { + "en" "Console" + "fr" "Console" + "ru" "Консоль" + "chi" "控制台" + "es" "Consola" + } + + "TW_Mode_Both" + { + "en" "Both" + "fr" "Les deux" + "ru" "Оба" + "chi" "两者" + "es" "Ambos" + } + + "TW_Menu_Cookie" + { + "en" "TriggerWatcher Settings" + "fr" "Parametres TriggerWatcher" + "ru" "Настройки TriggerWatcher" + "chi" "TriggerWatcher 设置" + "es" "Configuracion de TriggerWatcher" + } + + "TW_Menu_Title" + { + "en" "TriggerWatcher Settings" + "fr" "Parametres TriggerWatcher" + "ru" "Настройки TriggerWatcher" + "chi" "TriggerWatcher 设置" + "es" "Configuracion de TriggerWatcher" + } + + "TW_Menu_Buttons" + { + "en" "Buttons" + "fr" "Boutons" + "ru" "Кнопки" + "chi" "按钮" + "es" "Botones" + } + + "TW_Menu_Triggers" + { + "en" "Triggers" + "fr" "Declencheurs" + "ru" "Триггеры" + "chi" "触发器" + "es" "Disparadores" + } + + "TW_Menu_Buttons_Display" + { + "#format" "{1:s}" + "en" "Buttons: {1}" + "fr" "Boutons: {1}" + "ru" "Кнопки: {1}" + "chi" "按钮: {1}" + "es" "Botones: {1}" + } + + "TW_Menu_Triggers_Display" + { + "#format" "{1:s}" + "en" "Triggers: {1}" + "fr" "Declencheurs: {1}" + "ru" "Триггеры: {1}" + "chi" "触发器: {1}" + "es" "Disparadores: {1}" + } +} diff --git a/sourceknight.yaml b/sourceknight.yaml index 3382a66..6b7edf4 100644 --- a/sourceknight.yaml +++ b/sourceknight.yaml @@ -1,6 +1,6 @@ project: sourceknight: 0.1 - name: ButtonNotifier + name: TriggerWatcher dependencies: - name: sourcemod type: tar @@ -27,4 +27,4 @@ project: root: / output: /addons/sourcemod/plugins targets: - - ButtonNotifier + - TriggerWatcher From 8a5b4c417ecb539e10e8b2704038e502d35c3ef6 Mon Sep 17 00:00:00 2001 From: Rushaway Date: Mon, 16 Feb 2026 15:18:30 +0100 Subject: [PATCH 2/5] Create README.md for TriggerWatcher plugin Added initial README.md with project details and installation instructions. --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a581f7 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# TriggerWatcher + +TriggerWatcher logs button and trigger activations to chat and/or console for admins and SourceTV. It is the successor to the old ButtonNotifier plugin (repository name: sm-plugin-ButtonNotifier). + +## Features +- Notify modes per client: None, Chat, Console, or Both. +- Clean, short chat messages with a [TW] prefix. +- Detailed console/server logs with user ID and entity info. +- Spam throttling for rapid button presses. +- Optional EntWatch integration to ignore special items. + +## Installation +1) Compile (or download latest release) and install the plugin. +2) Place translations in the SourceMod translations folder. + +Files: +- Plugin: addons/sourcemod/plugins/TriggerWatcher.smx +- Translations: addons/sourcemod/translations/TriggerWatcher.phrases.txt + +## Configuration +Cvars: +- sm_TriggerWatcher_block_spam_delay (default 5) + - Spam notification delay in seconds. Set to 0 to disable spam blocking. + +## User Settings +Players can open the cookie menu entry "TriggerWatcher Settings" to select per-category display mode: +- Buttons +- Triggers + +> [!IMPORTANT] +> Upgrade Guide: v2 -> v3 +### This release is a rename and cleanup of the legacy ButtonNotifier plugin. + +Key changes: +- Plugin renamed: ButtonNotifier -> TriggerWatcher +- Repository renamed: sm-plugin-ButtonNotifier -> sm-plugin-TriggerWatcher +- Cookie key renamed: TriggerWatcher_display +- Cvar change: removed sm_TriggerWatcher_block_spam (delay now controls spam blocking) + +Recommended upgrade steps: +1) Replace the old .smx with TriggerWatcher.smx +2) Install new translations with TriggerWatcher.phrases.txt +3) Remove old configs/cookies if you want a clean slate + +## Notes +- Console logging uses detailed output including user ID and entity info. +- Chat logging is intentionally short to avoid spam. From 78ede70f490a212a47e73b7bdb4d1c05c5f207c5 Mon Sep 17 00:00:00 2001 From: Rushaway Date: Mon, 16 Feb 2026 15:30:59 +0100 Subject: [PATCH 3/5] Fix loop condition to include MaxClients --- addons/sourcemod/scripting/TriggerWatcher.sp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/sourcemod/scripting/TriggerWatcher.sp b/addons/sourcemod/scripting/TriggerWatcher.sp index 21f3e7e..0bbd640 100644 --- a/addons/sourcemod/scripting/TriggerWatcher.sp +++ b/addons/sourcemod/scripting/TriggerWatcher.sp @@ -82,7 +82,7 @@ public void OnPluginStart() if (!g_bLate) return; - for (int i = 1; i < MaxClients; i++) + for (int i = 1; i <= MaxClients; i++) { if (!IsClientConnected(i) || IsFakeClient(i) || !AreClientCookiesCached(i)) continue; @@ -437,4 +437,4 @@ void NotifyModeChat(int client, NotifyMode mode, const char[] detailPhrase) char detail[64]; FormatEx(detail, sizeof(detail), "%T", detailPhrase, client, label); CPrintToChat(client, "{red}%s {lightgreen}%s", TW_TAG, detail); -} \ No newline at end of file +} From 7dd1d7ef2886b242e19c2fc76f4b3b9562f644e5 Mon Sep 17 00:00:00 2001 From: Rushaway Date: Mon, 16 Feb 2026 15:53:24 +0100 Subject: [PATCH 4/5] Enhance spam detection and cookie menu title Updated spam delay convar to allow disabling spam detection. Changed cookie menu title to use formatted string. --- addons/sourcemod/scripting/TriggerWatcher.sp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/addons/sourcemod/scripting/TriggerWatcher.sp b/addons/sourcemod/scripting/TriggerWatcher.sp index 0bbd640..737f06b 100644 --- a/addons/sourcemod/scripting/TriggerWatcher.sp +++ b/addons/sourcemod/scripting/TriggerWatcher.sp @@ -68,12 +68,14 @@ public void OnPluginStart() } /* CONVARS */ - g_hCVar_SpamDelay = CreateConVar("sm_TriggerWatcher_block_spam_delay", "5", "Time to wait before notifying the next button press", FCVAR_NONE, true, 1.0, true, 60.0); + g_hCVar_SpamDelay = CreateConVar("sm_TriggerWatcher_block_spam_delay", "5", "Time to wait before notifying the next button press (0 = disable spam detection)", FCVAR_NONE, true, 0.0, true, 60.0); AutoExecConfig(true); /* COOKIES */ - SetCookieMenuItem(CookieHandler, 0, "TriggerWatcher Settings"); + char sCookieMenuTitle[64]; + FormatEx(sCookieMenuTitle, sizeof(sCookieMenuTitle), "%t", "TW_Menu_Cookie"); + SetCookieMenuItem(CookieHandler, 0, sCookieMenuTitle); g_hCookie_DisplayType = new Cookie("TriggerWatcher_display", "TriggerWatcher display method", CookieAccess_Private); /* HOOKS */ @@ -116,6 +118,7 @@ public void Event_RoundStart(Event hEvent, const char[] sName, bool bDontBroadca public void OnMapEnd() { UnhookEntityOutput("func_button", "OnPressed", ButtonPressed); + UnhookEntityOutput("func_rot_button", "OnPressed", ButtonPressed); UnhookEntityOutput("trigger_once", "OnTrigger", TriggerTouched); UnhookEntityOutput("trigger_multiple", "OnStartTouch", TriggerTouched); UnhookEntityOutput("trigger_teleport", "OnStartTouch", TriggerTouched); @@ -378,7 +381,7 @@ void NotifyButton(int activator, const char[] userid, const char[] entity, bool if (!ShouldNotifyClient(i)) continue; - NotifyMode mode = isSpam ? g_ClientState[i].triggersDisplay : g_ClientState[i].buttonsDisplay; + NotifyMode mode = g_ClientState[i].buttonsDisplay; if (mode == Notify_Console || mode == Notify_Both) { if (isSpam) From 27cd8835726d44445534630ee31128f27ef86a3e Mon Sep 17 00:00:00 2001 From: Rushaway Date: Mon, 16 Feb 2026 20:09:32 +0100 Subject: [PATCH 5/5] remove un-used translation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../sourcemod/translations/TriggerWatcher.phrases.txt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/addons/sourcemod/translations/TriggerWatcher.phrases.txt b/addons/sourcemod/translations/TriggerWatcher.phrases.txt index 1a3736b..64c0525 100644 --- a/addons/sourcemod/translations/TriggerWatcher.phrases.txt +++ b/addons/sourcemod/translations/TriggerWatcher.phrases.txt @@ -60,16 +60,6 @@ "es" "{1} {2} ({3}) spamea {4}" } - "TW_Display" - { - "#format" "{1:s},{2:s}" - "en" "{red}{1} {lightgreen}Display: {blue}{2}" - "fr" "{red}{1} {lightgreen}Affichage: {blue}{2}" - "ru" "{red}{1} {lightgreen}Отображение: {blue}{2}" - "chi" "{red}{1} {lightgreen}显示: {blue}{2}" - "es" "{red}{1} {lightgreen}Visualizacion: {blue}{2}" - } - "TW_Mode_None" { "en" "None"