Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
108 changes: 108 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# BoostAlert

BoostAlert is a SourceMod plugin for ZombieReloaded servers that detects CT -> ZM boost patterns and knife follow-up interactions, then notifies admins with:

- concise chat messages (localized)
- detailed console lines (localized)

## Features

- Detects high-impact boost hits from CT to T/ZM (shotguns/snipers).
- Detects knife hits from CT to T/ZM with configurable minimum damage.
- Detects follow-up infection/kill events after a recent knife hit.
- Detects boost-assisted infection chains in ZR.
- Sends notifications only to admins (or SourceTV).
- Logs relevant events to SourceMod logs.
- Exposes forwards for integrations with other plugins.
- Includes multilingual phrases: English, French, Spanish, Russian, Simplified Chinese.

Optional:

- `knifemode` (if present, alerts can be suppressed via cvar)

## Installation

1. Compile `addons/sourcemod/scripting/BoostAlert.sp` or download latest release.
2. Copy `BoostAlert.smx` to `addons/sourcemod/plugins/`.
3. Copy `addons/sourcemod/translations/BoostAlert.phrases.txt` to your server.
4. Restart map/server (or reload plugin).

## Configuration (ConVars)

### Knife

- `sm_knifenotifytime` (default: `5`)
- Time window (seconds) where a recently knifed zombie is tracked for follow-up events.
- `sm_knifemod_blocked` (default: `1`)
- If KnifeMode is loaded: `1` blocks alerts, `0` allows alerts.
- `sm_knifemin_damage` (default: `15`)
- Minimum knife damage to trigger a knife alert.

### Boost

- `sm_boostalert_hitgroup` (default: `1`)
- `0` = any hitgroup, `1` = head-only (event hitgroup match).
- `sm_boostalert_spam` (default: `3`)
- Anti-spam delay before another boost warning can be sent for the same target.
- `sm_boostalert_delay` (default: `15`)
- Time window where a boosted target can still trigger follow-up infection warning.
- `sm_boostalert_min_damage` (default: `80`)
- Minimum damage to trigger boost warning.

### Auth ID

- `sm_boostalert_authid` (default: `1`)
- Auth ID type in detailed output:
- `0` = Engine
- `1` = Steam2
- `2` = Steam3
- `3` = Steam64

## Notifications

### Chat

Chat notifications are compact and intended for quick admin awareness.

Examples:

- `[BA] Wyatt boosted Yahn (awp, -84 HP)`
- `[BA] Wyatt infected Yahn (Recently knifed by Rushaway)`

### Console

Console notifications are detailed and include userid/auth details.

Examples:

- `[BA] Wyatt (#1390|U:1:...) boosted Yahn (#1391|U:1:...) with awp (-84 HP)`
- `[BA] Wyatt (#...) infected Yahn (#...) (Recently knifed by Rushaway (#...))`

## Forwards

BoostAlert exposes two global forwards:

```pawn
BoostAlert_OnBoost(int attacker, int victim, int damage, const char[] weapon)
BoostAlert_OnBoostedKill(int attacker, int victim, int initialAttacker, int damage, const char[] weapon)
```

## Translation

Translation file:

- `addons/sourcemod/translations/BoostAlert.phrases.txt`

Supported language keys:

- `en`
- `fr`
- `es`
- `ru`
- `chi` (Simplified Chinese)

## Notes

- Boost detection weapon list is currently: `m3`, `xm1014`, `awp`, `scout`, `sg550`, `g3sg1`.
- Admin notification target is: SourceTV or users with `Admin_Generic`.
- Config is auto-generated.
194 changes: 131 additions & 63 deletions addons/sourcemod/scripting/BoostAlert.sp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include <multicolors>
#include <zombiereloaded>

#define BA_TAG "[BA]"

bool g_Plugin_ZR = false;
bool g_bPlugin_KnifeMode = false;

Expand Down Expand Up @@ -34,7 +36,7 @@ public Plugin myinfo =
name = "Boost Notifications",
description = "Notify admins when a zombie gets boosted",
author = "Kelyan3, Obus + BotoX, maxime1907, .Rushaway",
version = "2.1.1",
version = "3.0.0",
url = "https://github.com/srcdslab/sm-plugin-BoostAlert"
};

Expand All @@ -49,6 +51,8 @@ public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max

public void OnPluginStart()
{
LoadTranslations("BoostAlert.phrases");

// Knife Alert
g_cvNotificationTime = CreateConVar("sm_knifenotifytime", "5", "Time before a knifed zombie is considered \"not knifed\"", 0, true, 0.0, true, 60.0);
g_cvKnifeModMsgs = CreateConVar("sm_knifemod_blocked", "1", "Block Alert messages when KnifeMode library is detected [0 = Print Alert | 1 = Block Alert]");
Expand Down Expand Up @@ -146,11 +150,12 @@ void HandleKnifeAlert(int victim, int attacker, int damage)
g_iClientUserId[victim] = GetClientUserId(attacker);
g_iNotificationTime[victim] = (GetTime() + g_cvNotificationTime.IntValue);

char sMessage[1024];
Format(sMessage, sizeof(sMessage), "%L Knifed %L", attacker, victim);
LogMessage(sMessage);
LogMessage("%L Knifed %L (-%d HP)", attacker, victim, damage);

NotifyAdmins("{green}[SM] {blue}%N {default}knifed {red}%N{default}. (-%d HP)", attacker, victim, damage);
char sAttackerId[64], sVictimId[64];
BuildUserIdString(attacker, sAttackerId, sizeof(sAttackerId));
BuildUserIdString(victim, sVictimId, sizeof(sVictimId));
NotifyKnifeEvent(attacker, sAttackerId, victim, sVictimId, damage);
Forward_OnBoost(attacker, victim, damage, "knife");
}

Expand All @@ -161,43 +166,26 @@ void HandleKnifedZombieInfection(int victim, int attacker, int damage)
int pOldKnifer = GetClientOfUserId(g_iClientUserId[attacker]);
if (victim != pOldKnifer)
{
AuthIdType authType = view_as<AuthIdType>(GetConVarInt(g_cvAuthID));

char sMessage[1024], sAtkSID[64], OldKniferSteamID[64];
GetClientAuthId(attacker, authType, sAtkSID, sizeof(sAtkSID));
GetClientAuthId(pOldKnifer, authType, OldKniferSteamID, sizeof(OldKniferSteamID));

if (authType == AuthId_Steam3)
{
ReplaceString(sAtkSID, sizeof(sAtkSID), "[", "");
ReplaceString(sAtkSID, sizeof(sAtkSID), "]", "");
ReplaceString(OldKniferSteamID, sizeof(OldKniferSteamID), "[", "");
ReplaceString(OldKniferSteamID, sizeof(OldKniferSteamID), "]", "");
}

Format(sAtkSID, sizeof(sAtkSID), "#%d|%s", GetClientUserId(attacker), sAtkSID);
char sAtkSID[64], sVictimId[64];
BuildUserIdString(attacker, sAtkSID, sizeof(sAtkSID));
BuildUserIdString(victim, sVictimId, sizeof(sVictimId));

if (pOldKnifer != -1)
{
char sAtkAttackerName[MAX_NAME_LENGTH];
GetClientName(pOldKnifer, sAtkAttackerName, sizeof(sAtkAttackerName));

Format(sMessage, sizeof(sMessage), "%L %s %L (Recently knifed by %L)", attacker, g_Plugin_ZR ? "infected" : "killed", victim, pOldKnifer);
LogMessage(sMessage);

CPrintToChatAll("{green}[SM]{red} %N ({lightgreen}%s{red}){default} %s{blue} %N{default}.", attacker, sAtkSID, g_Plugin_ZR ? "infected" : "killed", victim);
CPrintToChatAll("{green}[SM]{default} Knifed by{blue} %s{default}.", sAtkAttackerName);
char sOldKniferId[64];
BuildUserIdString(pOldKnifer, sOldKniferId, sizeof(sOldKniferId));
LogMessage("%L %s %L (Recently knifed by %L)", attacker, g_Plugin_ZR ? "infected" : "killed", victim, pOldKnifer);

NotifyKnifeFollowupConnected(attacker, sAtkSID, victim, sVictimId, pOldKnifer, sOldKniferId, g_Plugin_ZR);
Forward_OnBoostedKill(attacker, victim, pOldKnifer, damage, "knife");
}
else
{
Format(sMessage, sizeof(sMessage), "%L %s %L (Recently knifed by a disconnected player %s)", attacker, g_Plugin_ZR ? "infected" : "killed", victim, OldKniferSteamID);
LogMessage(sMessage);

CPrintToChatAll("{green}[SM]{red} %N ({lightgreen}%s{red}){green} %s{blue} %N{default}.", attacker, sAtkSID, g_Plugin_ZR ? "infected" : "killed", victim);
CPrintToChatAll("{green}[SM]{default} Knifed by a disconnected player. {lightgreen}%s", OldKniferSteamID);
char sOldKniferSteamID[64];
BuildStoredUserIdString(g_iClientUserId[attacker], sOldKniferSteamID, sizeof(sOldKniferSteamID));
LogMessage("%L %s %L (Recently knifed by a disconnected player %s)", attacker, g_Plugin_ZR ? "infected" : "killed", victim, sOldKniferSteamID);

NotifyKnifeFollowupDisconnected(attacker, sAtkSID, victim, sVictimId, sOldKniferSteamID, g_Plugin_ZR);
Forward_OnBoostedKill(attacker, victim, -1, damage, "knife");
}
}
Expand All @@ -214,12 +202,13 @@ void HandleBoostAlert(int victim, int attacker, const char[] weapon, int damage)
if (time - g_cvBoostSpam.IntValue >= g_iGameTimeSpam[victim])
{
g_iGameTimeSpam[victim] = time;
NotifyAdmins("{green}[SM] {blue}%N {default}boosted {red}%N{default}. ({olive}%s{default})", attacker, victim, weapon);

char sMessage[1024];
Format(sMessage, sizeof(sMessage), "%L Boosted %L (%s)", attacker, victim, weapon);
LogMessage(sMessage);
char sAttackerId[64], sVictimId[64];
BuildUserIdString(attacker, sAttackerId, sizeof(sAttackerId));
BuildUserIdString(victim, sVictimId, sizeof(sVictimId));
NotifyBoostEvent(attacker, sAttackerId, victim, sVictimId, weapon, damage);

LogMessage("%L boosted %L with %s (-%d HP)", attacker, victim, weapon, damage);
Forward_OnBoost(attacker, victim, damage, weapon);
}
}
Expand All @@ -244,50 +233,129 @@ public void ZR_OnClientInfected(int client, int attacker, bool motherInfect, boo
return;

int time = GetTime();
if (client != g_iAttackerIDs[attacker] && attacker == g_iDamagedIDs[attacker]
&& g_iGameTimes[attacker] >= (time - g_cvBoostDelay.IntValue))
if (client != g_iAttackerIDs[attacker] && attacker == g_iDamagedIDs[attacker] && g_iGameTimes[attacker] >= (time - g_cvBoostDelay.IntValue))
{
if (IsValidClient(g_iAttackerIDs[attacker]) && IsValidClient(g_iDamagedIDs[attacker]))
{
AuthIdType authType = view_as<AuthIdType>(GetConVarInt(g_cvAuthID));
char sAttackerSteamID[64], sBoosterSteamID[64], sVictimId[64];
BuildUserIdString(g_iDamagedIDs[attacker], sAttackerSteamID, sizeof(sAttackerSteamID));
BuildUserIdString(g_iAttackerIDs[attacker], sBoosterSteamID, sizeof(sBoosterSteamID));
BuildUserIdString(client, sVictimId, sizeof(sVictimId));

char sAttackerSteamID[64], sBoosterSteamID[64];
GetClientAuthId(g_iDamagedIDs[attacker], authType, sAttackerSteamID, sizeof(sAttackerSteamID));
GetClientAuthId(g_iAttackerIDs[attacker], authType, sBoosterSteamID, sizeof(sBoosterSteamID));
NotifyBoostInfectionEvent(g_iDamagedIDs[attacker], sAttackerSteamID, client, sVictimId, g_iAttackerIDs[attacker], sBoosterSteamID);

if (authType == AuthId_Steam3)
{
ReplaceString(sAttackerSteamID, sizeof(sAttackerSteamID), "[", "");
ReplaceString(sAttackerSteamID, sizeof(sAttackerSteamID), "]", "");
ReplaceString(sBoosterSteamID, sizeof(sBoosterSteamID), "[", "");
ReplaceString(sBoosterSteamID, sizeof(sBoosterSteamID), "]", "");
}
Forward_OnBoostedKill(g_iDamagedIDs[attacker], client, g_iAttackerIDs[attacker], 1, "zombie_claws_of_death");
LogMessage("%L infected (%s) infected %L (%s), boosted by %L (%s)", g_iDamagedIDs[attacker], sAttackerSteamID, client, sVictimId, g_iAttackerIDs[attacker], sBoosterSteamID);
}
}

}

Format(sAttackerSteamID, sizeof(sAttackerSteamID), "#%d|%s", GetClientUserId(g_iDamagedIDs[attacker]), sAttackerSteamID);
Format(sBoosterSteamID, sizeof(sBoosterSteamID), "#%d|%s", GetClientUserId(g_iAttackerIDs[attacker]), sBoosterSteamID);
bool ShouldNotifyClient(int client)
{
return IsValidClient(client) && (IsClientSourceTV(client) || GetAdminFlag(GetUserAdmin(client), Admin_Generic));
}

NotifyAdmins("{green}[SM] {red}%N ({lightgreen}%s{red}) {default}infected {red}%N{default}, boosted by {blue}%N ({lightgreen}%s{blue}){default}.",
g_iDamagedIDs[attacker], sAttackerSteamID, client, g_iAttackerIDs[attacker], sBoosterSteamID);
void NotifyKnifeEvent(int attacker, const char[] attackerId, int victim, const char[] victimId, int damage)
{
for (int i = 1; i <= MaxClients; i++)
{
if (!ShouldNotifyClient(i))
continue;

Forward_OnBoostedKill(g_iDamagedIDs[attacker], client, g_iAttackerIDs[attacker], 1, "zombie_claws_of_death");
}
CPrintToChat(i, "%t", "BA_Chat_Knife", BA_TAG, attacker, victim, damage);
PrintToConsole(i, "%T", "BA_Console_Knife", i, BA_TAG, attacker, attackerId, victim, victimId, damage);
}
}

void NotifyAdmins(const char[] format, any ...)
void NotifyBoostEvent(int attacker, const char[] attackerId, int victim, const char[] victimId, const char[] weapon, int damage)
{
char buffer[512];
VFormat(buffer, sizeof(buffer), format, 2);
for (int i = 1; i <= MaxClients; i++)
{
if (!ShouldNotifyClient(i))
continue;

CPrintToChat(i, "%t", "BA_Chat_Boost", BA_TAG, attacker, victim, weapon, damage);
PrintToConsole(i, "%T", "BA_Console_Boost", i, BA_TAG, attacker, attackerId, victim, victimId, weapon, damage);
}
}

void NotifyKnifeFollowupConnected(int attacker, const char[] attackerId, int victim, const char[] victimId, int oldKnifer, const char[] oldKniferId, bool isInfection)
{
char chatKey[48], consoleKey[40];
strcopy(chatKey, sizeof(chatKey), isInfection ? "BA_Chat_Knife_Followup_Infect" : "BA_Chat_Knife_Followup_Kill");
strcopy(consoleKey, sizeof(consoleKey), isInfection ? "BA_Console_Knife_Followup_Infect" : "BA_Console_Knife_Followup_Kill");

for (int i = 1; i <= MaxClients; i++)
{
if (IsValidClient(i) && (IsClientSourceTV(i) || GetAdminFlag(GetUserAdmin(i), Admin_Generic)))
{
CPrintToChat(i, buffer);
}
if (!ShouldNotifyClient(i))
continue;

CPrintToChat(i, "%t", chatKey, BA_TAG, attacker, victim, oldKnifer);
PrintToConsole(i, "%T", consoleKey, i, BA_TAG, attacker, attackerId, victim, victimId, oldKnifer, oldKniferId);
}
}

void NotifyKnifeFollowupDisconnected(int attacker, const char[] attackerId, int victim, const char[] victimId, const char[] oldKniferId, bool isInfection)
{
char chatKey[61], consoleKey[47];
strcopy(chatKey, sizeof(chatKey), isInfection ? "BA_Chat_Knife_Followup_Infect_Disconnected" : "BA_Chat_Knife_Followup_Kill_Disconnected");
strcopy(consoleKey, sizeof(consoleKey), isInfection ? "BA_Console_Knife_Followup_Infect_Disconnected" : "BA_Console_Knife_Followup_Kill_Disconnected");

for (int i = 1; i <= MaxClients; i++)
{
if (!ShouldNotifyClient(i))
continue;

CPrintToChat(i, "%t", chatKey, BA_TAG, attacker, victim, oldKniferId);
PrintToConsole(i, "%T", consoleKey, i, BA_TAG, attacker, attackerId, victim, victimId, oldKniferId);
}
}

void NotifyBoostInfectionEvent(int attacker, const char[] attackerId, int victim, const char[] victimId, int booster, const char[] boosterId)
{
for (int i = 1; i <= MaxClients; i++)
{
if (!ShouldNotifyClient(i))
continue;

CPrintToChat(i, "%t", "BA_Chat_Boost_Infection", BA_TAG, attacker, victim, booster);
PrintToConsole(i, "%T", "BA_Console_Boost_Infection", i, BA_TAG, attacker, attackerId, victim, victimId, booster, boosterId);
}
}

void BuildUserIdString(int client, char[] buffer, int maxlen)
{
AuthIdType authType = view_as<AuthIdType>(g_cvAuthID.IntValue);
GetClientAuthId(client, authType, buffer, maxlen, false);

if (authType == AuthId_Steam3)
{
ReplaceString(buffer, maxlen, "[", "");
ReplaceString(buffer, maxlen, "]", "");
}

Format(buffer, maxlen, "#%d|%s", GetClientUserId(client), buffer);
}

void BuildStoredUserIdString(int userId, char[] buffer, int maxlen)
{
if (userId <= 0)
{
strcopy(buffer, maxlen, "unknown");
return;
}

int client = GetClientOfUserId(userId);
if (IsValidClient(client))
{
BuildUserIdString(client, buffer, maxlen);
return;
}

Format(buffer, maxlen, "#%d|disconnected", userId);
}

stock bool IsValidClient(int client)
{
if (client <= 0 || client > MaxClients || !IsClientConnected(client))
Expand Down
Loading