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/README.md b/README.md new file mode 100644 index 0000000..7a2037c --- /dev/null +++ b/README.md @@ -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. diff --git a/addons/sourcemod/scripting/BoostAlert.sp b/addons/sourcemod/scripting/BoostAlert.sp index 820f0b1..274e446 100644 --- a/addons/sourcemod/scripting/BoostAlert.sp +++ b/addons/sourcemod/scripting/BoostAlert.sp @@ -7,6 +7,8 @@ #include #include +#define BA_TAG "[BA]" + bool g_Plugin_ZR = false; bool g_bPlugin_KnifeMode = false; @@ -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" }; @@ -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]"); @@ -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"); } @@ -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(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"); } } @@ -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); } } @@ -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(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(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)) diff --git a/addons/sourcemod/translations/BoostAlert.phrases.txt b/addons/sourcemod/translations/BoostAlert.phrases.txt new file mode 100644 index 0000000..92625c5 --- /dev/null +++ b/addons/sourcemod/translations/BoostAlert.phrases.txt @@ -0,0 +1,142 @@ +"Phrases" +{ + "BA_Chat_Knife" + { + "#format" "{1:s},{2:N},{3:N},{4:d}" + "en" "{gold}{1} {deepskyblue}{2} {default}knifed {lightcoral}{3} {grey}(-{4} HP)" + "fr" "{gold}{1} {deepskyblue}{2} {default}a poignardé {lightcoral}{3} {grey}(-{4} HP)" + "es" "{gold}{1} {deepskyblue}{2} {default}acuchilló a {lightcoral}{3} {grey}(-{4} HP)" + "ru" "{gold}{1} {deepskyblue}{2} {default}ударил ножом {lightcoral}{3} {grey}(-{4} HP)" + "chi" "{gold}{1} {deepskyblue}{2} {default}刀了 {lightcoral}{3} {grey}(-{4} HP)" + } + + "BA_Console_Knife" + { + "#format" "{1:s},{2:N},{3:s},{4:N},{5:s},{6:d}" + "en" "{1} {2} ({3}) knifed {4} ({5}) (-{6} HP)" + "fr" "{1} {2} ({3}) a poignardé {4} ({5}) (-{6} HP)" + "es" "{1} {2} ({3}) acuchilló a {4} ({5}) (-{6} HP)" + "ru" "{1} {2} ({3}) ударил ножом {4} ({5}) (-{6} HP)" + "chi" "{1} {2} ({3}) 刀了 {4} ({5}) (-{6} HP)" + } + + "BA_Chat_Boost" + { + "#format" "{1:s},{2:N},{3:N},{4:s},{5:d}" + "en" "{gold}{1} {deepskyblue}{2} {default}boosted {lightcoral}{3} {grey}({4}, -{5} HP)" + "fr" "{gold}{1} {deepskyblue}{2} {default}a boosté {lightcoral}{3} {grey}({4}, -{5} HP)" + "es" "{gold}{1} {deepskyblue}{2} {default}hizo boost a {lightcoral}{3} {grey}({4}, -{5} HP)" + "ru" "{gold}{1} {deepskyblue}{2} {default}сделал буст по {lightcoral}{3} {grey}({4}, -{5} HP)" + "chi" "{gold}{1} {deepskyblue}{2} {default}boost 了 {lightcoral}{3} {grey}({4}, -{5} HP)" + } + + "BA_Console_Boost" + { + "#format" "{1:s},{2:N},{3:s},{4:N},{5:s},{6:s},{7:d}" + "en" "{1} {2} ({3}) boosted {4} ({5}) with {6} (-{7} HP)" + "fr" "{1} {2} ({3}) a boosté {4} ({5}) avec {6} (-{7} HP)" + "es" "{1} {2} ({3}) hizo boost a {4} ({5}) con {6} (-{7} HP)" + "ru" "{1} {2} ({3}) сделал буст по {4} ({5}) из {6} (-{7} HP)" + "chi" "{1} {2} ({3}) 用 {6} boost 了 {4} ({5}) (-{7} HP)" + } + + "BA_Chat_Knife_Followup_Infect" + { + "#format" "{1:s},{2:N},{3:N},{4:N}" + "en" "{gold}{1} {deepskyblue}{2} {default}infected {lightcoral}{3} {grey}(Recently knifed by {deepskyblue}{4}{grey})" + "fr" "{gold}{1} {deepskyblue}{2} {default}a infecté {lightcoral}{3} {grey}(Récemment knifé par {deepskyblue}{4}{grey})" + "es" "{gold}{1} {deepskyblue}{2} {default}infectó a {lightcoral}{3} {grey}(Acuchillado recientemente por {deepskyblue}{4}{grey})" + "ru" "{gold}{1} {deepskyblue}{2} {default}заразил {lightcoral}{3} {grey}(Недавно был ударен ножом: {deepskyblue}{4}{grey})" + "chi" "{gold}{1} {deepskyblue}{2} {default}感染了 {lightcoral}{3} {grey}(最近被 {deepskyblue}{4}{grey} 刀过)" + } + + "BA_Chat_Knife_Followup_Kill" + { + "#format" "{1:s},{2:N},{3:N},{4:N}" + "en" "{gold}{1} {deepskyblue}{2} {default}killed {lightcoral}{3} {grey}(Recently knifed by {deepskyblue}{4}{grey})" + "fr" "{gold}{1} {deepskyblue}{2} {default}a tué {lightcoral}{3} {grey}(Récemment knifé par {deepskyblue}{4}{grey})" + "es" "{gold}{1} {deepskyblue}{2} {default}mató a {lightcoral}{3} {grey}(Acuchillado recientemente por {deepskyblue}{4}{grey})" + "ru" "{gold}{1} {deepskyblue}{2} {default}убил {lightcoral}{3} {grey}(Недавно был ударен ножом: {deepskyblue}{4}{grey})" + "chi" "{gold}{1} {deepskyblue}{2} {default}击杀了 {lightcoral}{3} {grey}(最近被 {deepskyblue}{4}{grey} 刀过)" + } + + "BA_Chat_Knife_Followup_Infect_Disconnected" + { + "#format" "{1:s},{2:N},{3:N},{4:s}" + "en" "{gold}{1} {deepskyblue}{2} {default}infected {lightcoral}{3} {grey}(Recently knifed by a disconnected player: {deepskyblue}{4}{grey})" + "fr" "{gold}{1} {deepskyblue}{2} {default}a infecté {lightcoral}{3} {grey}(Récemment knifé par un joueur déconnecté: {deepskyblue}{4}{grey})" + "es" "{gold}{1} {deepskyblue}{2} {default}infectó a {lightcoral}{3} {grey}(Acuchillado recientemente por un jugador desconectado: {deepskyblue}{4}{grey})" + "ru" "{gold}{1} {deepskyblue}{2} {default}заразил {lightcoral}{3} {grey}(Недавно был ударен ножом отключившимся игроком: {deepskyblue}{4}{grey})" + "chi" "{gold}{1} {deepskyblue}{2} {default}感染了 {lightcoral}{3} {grey}(最近被已离线玩家刀过: {deepskyblue}{4}{grey})" + } + + "BA_Chat_Knife_Followup_Kill_Disconnected" + { + "#format" "{1:s},{2:N},{3:N},{4:s}" + "en" "{gold}{1} {deepskyblue}{2} {default}killed {lightcoral}{3} {grey}(Recently knifed by a disconnected player: {deepskyblue}{4}{grey})" + "fr" "{gold}{1} {deepskyblue}{2} {default}a tué {lightcoral}{3} {grey}(Récemment knifé par un joueur déconnecté: {deepskyblue}{4}{grey})" + "es" "{gold}{1} {deepskyblue}{2} {default}mató a {lightcoral}{3} {grey}(Acuchillado recientemente por un jugador desconectado: {deepskyblue}{4}{grey})" + "ru" "{gold}{1} {deepskyblue}{2} {default}убил {lightcoral}{3} {grey}(Недавно был ударен ножом отключившимся игроком: {deepskyblue}{4}{grey})" + "chi" "{gold}{1} {deepskyblue}{2} {default}击杀了 {lightcoral}{3} {grey}(最近被已离线玩家刀过: {deepskyblue}{4}{grey})" + } + + "BA_Console_Knife_Followup_Infect" + { + "#format" "{1:s},{2:N},{3:s},{4:N},{5:s},{6:N},{7:s}" + "en" "{1} {2} ({3}) infected {4} ({5}) (Recently knifed by {6} ({7}))" + "fr" "{1} {2} ({3}) a infecté {4} ({5}) (Récemment knifé par {6} ({7}))" + "es" "{1} {2} ({3}) infectó a {4} ({5}) (Acuchillado recientemente por {6} ({7}))" + "ru" "{1} {2} ({3}) заразил {4} ({5}) (Недавно был ударен ножом: {6} ({7}))" + "chi" "{1} {2} ({3}) 感染了 {4} ({5})(最近被 {6} ({7}) 刀过)" + } + + "BA_Console_Knife_Followup_Kill" + { + "#format" "{1:s},{2:N},{3:s},{4:N},{5:s},{6:N},{7:s}" + "en" "{1} {2} ({3}) killed {4} ({5}) (Recently knifed by {6} ({7}))" + "fr" "{1} {2} ({3}) a tué {4} ({5}) (Récemment knifé par {6} ({7}))" + "es" "{1} {2} ({3}) mató a {4} ({5}) (Acuchillado recientemente por {6} ({7}))" + "ru" "{1} {2} ({3}) убил {4} ({5}) (Недавно был ударен ножом: {6} ({7}))" + "chi" "{1} {2} ({3}) 击杀了 {4} ({5})(最近被 {6} ({7}) 刀过)" + } + + "BA_Console_Knife_Followup_Infect_Disconnected" + { + "#format" "{1:s},{2:N},{3:s},{4:N},{5:s},{6:s}" + "en" "{1} {2} ({3}) infected {4} ({5}) (Recently knifed by a disconnected player: {6})" + "fr" "{1} {2} ({3}) a infecté {4} ({5}) (Récemment knifé par un joueur déconnecté: {6})" + "es" "{1} {2} ({3}) infectó a {4} ({5}) (Acuchillado recientemente por un jugador desconectado: {6})" + "ru" "{1} {2} ({3}) заразил {4} ({5}) (Недавно был ударен ножом отключившимся игроком: {6})" + "chi" "{1} {2} ({3}) 感染了 {4} ({5})(最近被已离线玩家刀过:{6})" + } + + "BA_Console_Knife_Followup_Kill_Disconnected" + { + "#format" "{1:s},{2:N},{3:s},{4:N},{5:s},{6:s}" + "en" "{1} {2} ({3}) killed {4} ({5}) (Recently knifed by a disconnected player: {6})" + "fr" "{1} {2} ({3}) a tué {4} ({5}) (Récemment knifé par un joueur déconnecté: {6})" + "es" "{1} {2} ({3}) mató a {4} ({5}) (Acuchillado recientemente por un jugador desconectado: {6})" + "ru" "{1} {2} ({3}) убил {4} ({5}) (Недавно был ударен ножом отключившимся игроком: {6})" + "chi" "{1} {2} ({3}) 击杀了 {4} ({5})(最近被已离线玩家刀过:{6})" + } + + "BA_Chat_Boost_Infection" + { + "#format" "{1:s},{2:N},{3:N},{4:N}" + "en" "{gold}{1} {deepskyblue}{2} {default}infected {lightcoral}{3} {grey}(boosted by {deepskyblue}{4}{grey})" + "fr" "{gold}{1} {deepskyblue}{2} {default}a infecté {lightcoral}{3} {grey}(boost par {deepskyblue}{4}{grey})" + "es" "{gold}{1} {deepskyblue}{2} {default}infectó a {lightcoral}{3} {grey}(boost por {deepskyblue}{4}{grey})" + "ru" "{gold}{1} {deepskyblue}{2} {default}заразил {lightcoral}{3} {grey}(буст от {deepskyblue}{4}{grey})" + "chi" "{gold}{1} {deepskyblue}{2} {default}感染了 {lightcoral}{3} {grey}(boost 来自 {deepskyblue}{4}{grey})" + } + + "BA_Console_Boost_Infection" + { + "#format" "{1:s},{2:N},{3:s},{4:N},{5:s},{6:N},{7:s}" + "en" "{1} {2} ({3}) infected {4} ({5}), boosted by {6} ({7})" + "fr" "{1} {2} ({3}) a infecté {4} ({5}), boost par {6} ({7})" + "es" "{1} {2} ({3}) infectó a {4} ({5}), boost por {6} ({7})" + "ru" "{1} {2} ({3}) заразил {4} ({5}), буст от {6} ({7})" + "chi" "{1} {2} ({3}) 感染了 {4} ({5}),由 {6} ({7}) 使用 boost" + } +}