diff --git a/AMBuilder b/AMBuilder
index 23c072a4..64ed4706 100644
--- a/AMBuilder
+++ b/AMBuilder
@@ -70,6 +70,7 @@ for sdk_target in MMSPlugin.sdk_targets:
'src/entitylistener.cpp',
'src/leader.cpp',
'src/buttonwatch.cpp',
+ 'src/topdefender.cpp',
'src/idlemanager.cpp',
'sdk/entity2/entitysystem.cpp',
'sdk/entity2/entityidentity.cpp',
diff --git a/CS2Fixes.vcxproj b/CS2Fixes.vcxproj
index 524de185..466bd0d3 100644
--- a/CS2Fixes.vcxproj
+++ b/CS2Fixes.vcxproj
@@ -207,6 +207,7 @@
+
@@ -282,6 +283,7 @@
+
diff --git a/CS2Fixes.vcxproj.filters b/CS2Fixes.vcxproj.filters
index 4b15140a..b7d41e59 100644
--- a/CS2Fixes.vcxproj.filters
+++ b/CS2Fixes.vcxproj.filters
@@ -197,6 +197,9 @@
Source Files\cs2_sdk\entity
+
+ Source Files
+
@@ -421,5 +424,8 @@
Header Files\cs2_sdk\entity
+
+ Header Files
+
\ No newline at end of file
diff --git a/cfg/cs2fixes/cs2fixes.cfg b/cfg/cs2fixes/cs2fixes.cfg
index 286f5699..2a87b0db 100644
--- a/cfg/cs2fixes/cs2fixes.cfg
+++ b/cfg/cs2fixes/cs2fixes.cfg
@@ -6,7 +6,6 @@ cs2f_weapons_enable 0 // Whether to enable weapon commands
cs2f_stopsound_enable 0 // Whether to enable stopsound
cs2f_noblock_enable 0 // Whether to use player noblock, which sets debris collision on every player
cs2f_noblock_grenades 0 // Whether to use noblock on grenade projectiles
-cs2f_topdefender_enable 0 // Whether to use TopDefender
cs2f_block_team_messages 0 // Whether to block team join messages
cs2f_movement_unlocker_enable 0 // Whether to enable movement unlocker
cs2f_use_old_push 0 // Whether to use the old CSGO trigger_push behavior (Necessary for surf and other modes that heavily use ported pushes)
@@ -60,6 +59,15 @@ cs2f_hide_distance_default 250 // The default distance for hide
cs2f_hide_distance_max 2000 // The max distance for hide
cs2f_hide_teammates_only 0 // Whether to hide teammates only
+// TopDefender settings
+cs2f_topdefender_enable 0 // Whether to use TopDefender
+cs2f_topdefender_scoreboard 0 // Whether to display defender rank on scoreboard as MVP count
+cs2f_topdefender_print 1 // Whether to print defender stats to console at round end
+cs2f_topdefender_score 5000 // Score given to the top defender
+cs2f_topdefender_threshold 1000 // Damage threshold for Top Defenders to be shown on round end
+cs2f_topdefender_rate 1.0 // How often TopDefender stats get updated
+cs2f_topdefender_clantag "[Top Defender]" // Clan tag given to the top defender
+
// Chat flood settings
cs2f_flood_interval 0.75 // Amount of time allowed between chat messages acquiring flood tokens
cs2f_max_flood_tokens 3 // Maximum number of flood tokens allowed before chat messages are blocked
diff --git a/src/cs2_sdk/entity/ccsplayercontroller.h b/src/cs2_sdk/entity/ccsplayercontroller.h
index f4bb5772..8b861366 100644
--- a/src/cs2_sdk/entity/ccsplayercontroller.h
+++ b/src/cs2_sdk/entity/ccsplayercontroller.h
@@ -46,6 +46,10 @@ class CCSPlayerController : public CBasePlayerController
SCHEMA_FIELD(int32_t, m_iRoundsWon)
SCHEMA_FIELD(int32_t, m_iMVPs)
SCHEMA_FIELD(float, m_flSmoothedPing)
+ SCHEMA_FIELD(bool, m_bMvpNoMusic)
+ SCHEMA_FIELD(int32_t, m_eMvpReason)
+ SCHEMA_FIELD(int32_t, m_iMusicKitID)
+ SCHEMA_FIELD(int32_t, m_iMusicKitMVPs)
static CCSPlayerController* FromPawn(CCSPlayerPawn* pawn)
{
diff --git a/src/events.cpp b/src/events.cpp
index 23b1e527..ad32f4c5 100644
--- a/src/events.cpp
+++ b/src/events.cpp
@@ -32,6 +32,7 @@
#include "map_votes.h"
#include "panoramavote.h"
#include "recipientfilters.h"
+#include "topdefender.h"
#include "votemanager.h"
#include "zombiereborn.h"
@@ -160,27 +161,10 @@ GAME_EVENT_F(player_spawn)
pItemServices->GiveNamedItem("item_assaultsuit");
}
-CConVar g_cvarEnableTopDefender("cs2f_topdefender_enable", FCVAR_NONE, "Whether to use TopDefender", false);
-
GAME_EVENT_F(player_hurt)
{
- if (!g_cvarEnableTopDefender.Get())
- return;
-
- CCSPlayerController* pAttacker = (CCSPlayerController*)pEvent->GetPlayerController("attacker");
- CCSPlayerController* pVictim = (CCSPlayerController*)pEvent->GetPlayerController("userid");
-
- // Ignore Ts/zombies and CTs hurting themselves
- if (!pAttacker || pAttacker->m_iTeamNum() != CS_TEAM_CT || pAttacker->m_iTeamNum() == pVictim->m_iTeamNum())
- return;
-
- ZEPlayer* pPlayer = pAttacker->GetZEPlayer();
-
- if (!pPlayer)
- return;
-
- pPlayer->SetTotalDamage(pPlayer->GetTotalDamage() + pEvent->GetInt("dmg_health"));
- pPlayer->SetTotalHits(pPlayer->GetTotalHits() + 1);
+ if (g_cvarEnableTopDefender.Get())
+ TD_OnPlayerHurt(pEvent);
}
GAME_EVENT_F(player_death)
@@ -191,22 +175,8 @@ GAME_EVENT_F(player_death)
if (g_cvarEnableEntWatch.Get())
EW_PlayerDeath(pEvent);
- if (!g_cvarEnableTopDefender.Get())
- return;
-
- CCSPlayerController* pAttacker = (CCSPlayerController*)pEvent->GetPlayerController("attacker");
- CCSPlayerController* pVictim = (CCSPlayerController*)pEvent->GetPlayerController("userid");
-
- // Ignore Ts/zombie kills and ignore CT teamkilling or suicide
- if (!pAttacker || !pVictim || pAttacker->m_iTeamNum != CS_TEAM_CT || pAttacker->m_iTeamNum == pVictim->m_iTeamNum)
- return;
-
- ZEPlayer* pPlayer = pAttacker->GetZEPlayer();
-
- if (!pPlayer)
- return;
-
- pPlayer->SetTotalKills(pPlayer->GetTotalKills() + 1);
+ if (g_cvarEnableTopDefender.Get())
+ TD_OnPlayerDeath(pEvent);
}
CConVar g_cvarFullAllTalk("cs2f_full_alltalk", FCVAR_NONE, "Whether to enforce sv_full_alltalk 1", false);
@@ -229,20 +199,8 @@ GAME_EVENT_F(round_start)
if (g_cvarFixHudFlashing.Get() && g_pGameRules && g_pGameRules->m_bWarmupPeriod)
g_pEngineServer2->ServerCommand("mp_warmup_end");
- if (!g_cvarEnableTopDefender.Get() || !GetGlobals())
- return;
-
- for (int i = 0; i < GetGlobals()->maxClients; i++)
- {
- ZEPlayer* pPlayer = g_playerManager->GetPlayer(i);
-
- if (!pPlayer)
- continue;
-
- pPlayer->SetTotalDamage(0);
- pPlayer->SetTotalHits(0);
- pPlayer->SetTotalKills(0);
- }
+ if (g_cvarEnableTopDefender.Get())
+ TD_OnRoundStart(pEvent);
}
GAME_EVENT_F(round_end)
@@ -250,51 +208,8 @@ GAME_EVENT_F(round_end)
if (g_cvarFixHudFlashing.Get() && g_pGameRules)
g_pGameRules->m_bGameRestart = false;
- if (!g_cvarEnableTopDefender.Get() || !GetGlobals())
- return;
-
- CUtlVector sortedPlayers;
-
- for (int i = 0; i < GetGlobals()->maxClients; i++)
- {
- ZEPlayer* pPlayer = g_playerManager->GetPlayer(i);
-
- if (!pPlayer || pPlayer->GetTotalDamage() == 0)
- continue;
-
- CCSPlayerController* pController = CCSPlayerController::FromSlot(pPlayer->GetPlayerSlot());
-
- if (!pController)
- continue;
-
- sortedPlayers.AddToTail(pPlayer);
- }
-
- if (sortedPlayers.Count() == 0)
- return;
-
- sortedPlayers.Sort([](ZEPlayer* const* a, ZEPlayer* const* b) -> int {
- return (*a)->GetTotalDamage() < (*b)->GetTotalDamage();
- });
-
- ClientPrintAll(HUD_PRINTTALK, " \x09TOP DEFENDERS");
-
- char colorMap[] = {'\x10', '\x08', '\x09', '\x0B'};
-
- for (int i = 0; i < sortedPlayers.Count(); i++)
- {
- ZEPlayer* pPlayer = sortedPlayers[i];
- CCSPlayerController* pController = CCSPlayerController::FromSlot(pPlayer->GetPlayerSlot());
-
- if (i < 5)
- ClientPrintAll(HUD_PRINTTALK, " %c%i. %s \x01- \x07%i DMG \x05(%i HITS & %i KILLS)", colorMap[MIN(i, 3)], i + 1, pController->GetPlayerName(), pPlayer->GetTotalDamage(), pPlayer->GetTotalHits(), pPlayer->GetTotalKills());
- else
- ClientPrint(pController, HUD_PRINTTALK, " \x0C%i. %s \x01- \x07%i DMG \x05(%i HITS & %i KILLS)", i + 1, pController->GetPlayerName(), pPlayer->GetTotalDamage(), pPlayer->GetTotalHits(), pPlayer->GetTotalKills());
-
- pPlayer->SetTotalDamage(0);
- pPlayer->SetTotalHits(0);
- pPlayer->SetTotalKills(0);
- }
+ if (g_cvarEnableTopDefender.Get())
+ TD_OnRoundEnd(pEvent);
}
GAME_EVENT_F(round_freeze_end)
diff --git a/src/playermanager.h b/src/playermanager.h
index 39316bd6..fd5165d2 100644
--- a/src/playermanager.h
+++ b/src/playermanager.h
@@ -165,6 +165,7 @@ class ZEPlayer
m_bConnected = false;
m_iTotalDamage = 0;
m_iTotalHits = 0;
+ m_iTotalHeadshots = 0;
m_iTotalKills = 0;
m_bVotedRTV = false;
m_bVotedExtend = false;
@@ -196,6 +197,7 @@ class ZEPlayer
m_flEntwatchHudX = -7.5f;
m_flEntwatchHudY = -2.0f;
m_flEntwatchHudSize = 60.0f;
+ m_bTopDefender = false;
}
~ZEPlayer()
@@ -230,6 +232,7 @@ class ZEPlayer
void SetHideDistance(int distance);
void SetTotalDamage(int damage) { m_iTotalDamage = damage; }
void SetTotalHits(int hits) { m_iTotalHits = hits; }
+ void SetTotalHeadshots(int headshots) { m_iTotalHeadshots = headshots; }
void SetTotalKills(int kills) { m_iTotalKills = kills; }
void SetRTVVote(bool bRTVVote) { m_bVotedRTV = bRTVVote; }
void SetRTVVoteTime(float flCurtime) { m_flRTVVoteTime = flCurtime; }
@@ -265,6 +268,7 @@ class ZEPlayer
void SetEntwatchHudColor(Color colorHud);
void SetEntwatchHudPos(float x, float y);
void SetEntwatchHudSize(float flSize);
+ void SetTopDefenderStatus(bool bStatus) { m_bTopDefender = bStatus; }
uint64 GetAdminFlags() { return m_iAdminFlags; }
int GetAdminImmunity() { return m_iAdminImmunity; }
@@ -276,6 +280,7 @@ class ZEPlayer
CPlayerSlot GetPlayerSlot() { return m_slot; }
int GetTotalDamage() { return m_iTotalDamage; }
int GetTotalHits() { return m_iTotalHits; }
+ int GetTotalHeadshots() { return m_iTotalHeadshots; }
int GetTotalKills() { return m_iTotalKills; }
bool GetRTVVote() { return m_bVotedRTV; }
float GetRTVVoteTime() { return m_flRTVVoteTime; }
@@ -313,6 +318,7 @@ class ZEPlayer
float GetEntwatchHudX() { return m_flEntwatchHudX; }
float GetEntwatchHudY() { return m_flEntwatchHudY; }
float GetEntwatchHudSize() { return m_flEntwatchHudSize; }
+ bool GetTopDefenderStatus() { return m_bTopDefender; }
void OnSpawn();
void OnAuthenticated();
@@ -346,6 +352,7 @@ class ZEPlayer
CBitVec m_shouldTransmit;
int m_iTotalDamage;
int m_iTotalHits;
+ int m_iTotalHeadshots;
int m_iTotalKills;
bool m_bVotedRTV;
float m_flRTVVoteTime;
@@ -385,6 +392,7 @@ class ZEPlayer
float m_flEntwatchHudX;
float m_flEntwatchHudY;
float m_flEntwatchHudSize;
+ bool m_bTopDefender;
};
class CPlayerManager
diff --git a/src/topdefender.cpp b/src/topdefender.cpp
new file mode 100644
index 00000000..b0091b48
--- /dev/null
+++ b/src/topdefender.cpp
@@ -0,0 +1,360 @@
+/**
+ * =============================================================================
+ * CS2Fixes
+ * Copyright (C) 2023-2025 Source2ZE
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * This program 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 the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ */
+
+#include "topdefender.h"
+#include "commands.h"
+#include "common.h"
+#include "ctimer.h"
+#include "detours.h"
+#include "entity/ccsplayercontroller.h"
+#include "idlemanager.h"
+#include "playermanager.h"
+
+std::vector sortedPlayers;
+std::vector failMessage = {
+ "No top defenders? You all are bad.",
+ "Start defending, less doorhugging.",
+ "Hold left-click to shoot, FYI",
+ "WHAT ARE YOU DOING?!?!?"};
+
+std::weak_ptr m_pUpdateTimer;
+
+CConVar g_cvarEnableTopDefender("cs2f_topdefender_enable", FCVAR_NONE, "Whether to use TopDefender", false);
+CConVar g_cvarTopDefenderScoreboard("cs2f_topdefender_scoreboard", FCVAR_NONE, "Whether to display defender rank on scoreboard as MVP count", false);
+CConVar g_cvarTopDefenderPrint("cs2f_topdefender_print", FCVAR_NONE, "Whether to print defender ranks to console at round end", true);
+CConVar g_cvarTopDefenderRate("cs2f_topdefender_rate", FCVAR_NONE, "How often TopDefender stats get updated", 1.0f, true, 0.1f, false, 0.0f);
+CConVar g_cvarTopDefenderThreshold("cs2f_topdefender_threshold", FCVAR_NONE, "Damage threshold for Top Defenders to be shown on round end", 1000, true, 0, false, 0);
+CConVar g_cvarTopDefenderScore("cs2f_topdefender_score", FCVAR_NONE, "Score given to the top defender", 5000, true, 0, false, 0);
+CConVar g_cvarTopDefenderClanTag("cs2f_topdefender_clantag", FCVAR_NONE, "Clan tag given to the top defender", "[Top Defender]");
+
+// Array sorting function
+bool SortTD(ZEPlayerHandle a, ZEPlayerHandle b)
+{
+ if (a.Get() && b.Get())
+ return a.Get()->GetTotalDamage() > b.Get()->GetTotalDamage();
+ else
+ return false;
+}
+
+void UnfuckMVP(CCSPlayerController* pController)
+{
+ // stop this shit from spamming mvp music
+ if (!pController->m_bMvpNoMusic())
+ pController->m_bMvpNoMusic = true;
+
+ if (pController->m_eMvpReason() != 0)
+ pController->m_eMvpReason = 0;
+
+ if (pController->m_iMusicKitID() != 0)
+ pController->m_iMusicKitID = 0;
+
+ if (pController->m_iMusicKitMVPs() != 0)
+ pController->m_iMusicKitMVPs = 0;
+}
+
+void TD_OnPlayerHurt(IGameEvent* pEvent)
+{
+ CCSPlayerController* pAttacker = (CCSPlayerController*)pEvent->GetPlayerController("attacker");
+ CCSPlayerController* pVictim = (CCSPlayerController*)pEvent->GetPlayerController("userid");
+
+ // Ignore Ts/zombies and CTs hurting themselves
+ if (!pAttacker || pAttacker->m_iTeamNum() != CS_TEAM_CT || pAttacker->m_iTeamNum() == pVictim->m_iTeamNum())
+ return;
+
+ ZEPlayer* pAttackerPlayer = pAttacker->GetZEPlayer();
+ ZEPlayer* pVictimPlayer = pVictim->GetZEPlayer();
+
+ if (!pAttackerPlayer || !pVictimPlayer)
+ return;
+
+ if (g_cvarIdleKickTime.Get() <= 0.0f || (std::time(0) - pVictimPlayer->GetLastInputTime()) < 15)
+ {
+ pAttackerPlayer->SetTotalDamage(pAttackerPlayer->GetTotalDamage() + pEvent->GetInt("dmg_health"));
+ pAttackerPlayer->SetTotalHits(pAttackerPlayer->GetTotalHits() + 1);
+
+ if (pEvent->GetInt("hitgroup") == 1)
+ pAttackerPlayer->SetTotalHeadshots(pAttackerPlayer->GetTotalHeadshots() + 1);
+ }
+}
+
+void TD_OnPlayerDeath(IGameEvent* pEvent)
+{
+ CCSPlayerController* pAttacker = (CCSPlayerController*)pEvent->GetPlayerController("attacker");
+ CCSPlayerController* pVictim = (CCSPlayerController*)pEvent->GetPlayerController("userid");
+
+ // Ignore Ts/zombie kills and ignore CT teamkilling or suicide
+ if (!pAttacker || !pVictim || pAttacker->m_iTeamNum != CS_TEAM_CT || pAttacker->m_iTeamNum == pVictim->m_iTeamNum)
+ return;
+
+ ZEPlayer* pPlayer = pAttacker->GetZEPlayer();
+ if (pPlayer)
+ pPlayer->SetTotalKills(pPlayer->GetTotalKills() + 1);
+}
+
+void TD_OnRoundStart(IGameEvent* pEvent)
+{
+ if (!GetGlobals())
+ return;
+
+ // Reset player information
+ for (int i = 0; i < GetGlobals()->maxClients; i++)
+ {
+ ZEPlayer* pPlayer = g_playerManager->GetPlayer(i);
+ if (!pPlayer)
+ continue;
+
+ pPlayer->SetTotalDamage(0);
+ pPlayer->SetTotalHits(0);
+ pPlayer->SetTotalKills(0);
+ pPlayer->SetTotalHeadshots(0);
+
+ CCSPlayerController* pController = CCSPlayerController::FromSlot(pPlayer->GetPlayerSlot());
+ if (pController)
+ {
+ if (g_cvarTopDefenderScoreboard.Get() && pController->m_iMVPs() != 0)
+ {
+ UnfuckMVP(pController);
+ pController->m_iMVPs = 0;
+ }
+
+ // If player is top defender, we set their score and clan tag
+ if (g_cvarTopDefenderScore.Get() > 0 && pPlayer->GetTopDefenderStatus())
+ {
+ pController->m_iScore() = pController->m_iScore() + g_cvarTopDefenderScore.Get();
+ pController->SetClanTag(g_cvarTopDefenderClanTag.Get().String());
+ }
+ }
+ }
+
+ m_pUpdateTimer = CTimer::Create(g_cvarTopDefenderRate.Get(), TIMERFLAG_MAP | TIMERFLAG_ROUND, []() {
+ if (!GetGlobals())
+ return -1.0f;
+
+ sortedPlayers.clear();
+
+ for (int i = 0; i < GetGlobals()->maxClients; i++)
+ {
+ ZEPlayer* pPlayer = g_playerManager->GetPlayer(i);
+ if (!pPlayer || pPlayer->GetTotalDamage() == 0)
+ continue;
+
+ sortedPlayers.push_back(pPlayer->GetHandle());
+ }
+
+ std::sort(sortedPlayers.begin(), sortedPlayers.end(), SortTD);
+
+ if (g_cvarTopDefenderScoreboard.Get())
+ {
+ for (int i = 0; i < sortedPlayers.size(); i++)
+ {
+ ZEPlayer* pPlayer = sortedPlayers[i].Get();
+ if (!pPlayer)
+ continue;
+
+ CCSPlayerController* pController = CCSPlayerController::FromSlot(pPlayer->GetPlayerSlot());
+ if (!pController)
+ continue;
+
+ if (pController->m_iMVPs() != i + 1)
+ {
+ UnfuckMVP(pController);
+ pController->m_iMVPs = i + 1;
+ }
+ }
+ }
+
+ return g_cvarTopDefenderRate.Get();
+ });
+}
+
+void TD_OnRoundEnd(IGameEvent* pEvent)
+{
+ // When the round ends, stop the timer and do final recalculation
+ if (!m_pUpdateTimer.expired())
+ m_pUpdateTimer.lock()->Cancel();
+
+ if (!GetGlobals())
+ return;
+
+ sortedPlayers.clear();
+
+ for (int i = 0; i < GetGlobals()->maxClients; i++)
+ {
+ ZEPlayer* pPlayer = g_playerManager->GetPlayer(i);
+ if (!pPlayer)
+ continue;
+
+ // Only add players over the threshold to the array
+ if (pPlayer->GetTotalDamage() >= g_cvarTopDefenderThreshold.Get())
+ sortedPlayers.push_back(pPlayer->GetHandle());
+
+ // Reset the current top defender
+ if (pPlayer->GetTopDefenderStatus())
+ {
+ CCSPlayerController* pController = CCSPlayerController::FromSlot(pPlayer->GetPlayerSlot());
+ if (pController)
+ {
+ pPlayer->SetTopDefenderStatus(false);
+ pController->m_iScore() = pController->m_iScore() - g_cvarTopDefenderScore.Get();
+ pController->SetClanTag("");
+ }
+ }
+ }
+
+ ClientPrintAll(HUD_PRINTTALK, " \x09*** TOP DEFENDERS ***");
+
+ // Check if players damaged more than threshold
+ if (sortedPlayers.size() == 0)
+ {
+ ClientPrintAll(HUD_PRINTTALK, " \x02%s", failMessage[rand() % failMessage.size()].c_str());
+ return;
+ }
+
+ std::sort(sortedPlayers.begin(), sortedPlayers.end(), SortTD);
+
+ char colorMap[] = {'\x10', '\x08', '\x09', '\x0B'};
+
+ for (int i = 0; i < sortedPlayers.size(); i++)
+ {
+ ZEPlayer* pPlayer = sortedPlayers[i].Get();
+ if (!pPlayer)
+ continue;
+
+ CCSPlayerController* pController = CCSPlayerController::FromSlot(pPlayer->GetPlayerSlot());
+ if (!pController)
+ continue;
+
+ if (i < 5)
+ ClientPrintAll(HUD_PRINTTALK, " %c%i. %s \x01- \x07%i DMG \x05(%i HITS | %.0f%% HS | %i KILL%s)", colorMap[MIN(i, 3)], i + 1, pController->GetPlayerName(), pPlayer->GetTotalDamage(), pPlayer->GetTotalHits(), ((double)pPlayer->GetTotalHeadshots() / (double)pPlayer->GetTotalHits()) * 100.0f, pPlayer->GetTotalKills(), pPlayer->GetTotalKills() == 1 ? "" : "S");
+ else
+ ClientPrint(pController, HUD_PRINTTALK, " \x0C%i. %s \x01- \x07%i DMG \x05(%i HITS | %.0f%% HS | %i KILL%s)", i + 1, pController->GetPlayerName(), pPlayer->GetTotalDamage(), pPlayer->GetTotalHits(), ((double)pPlayer->GetTotalHeadshots() / (double)pPlayer->GetTotalHits()) * 100.0f, pPlayer->GetTotalKills(), pPlayer->GetTotalKills() == 1 ? "" : "S");
+
+ if (i == 0)
+ pPlayer->SetTopDefenderStatus(true);
+ }
+
+ // Because there are other round end stats to be displayed, delay printing it by a second to mitigate conflicts
+ if (g_cvarTopDefenderPrint.Get())
+ {
+ CTimer::Create(1.0f, TIMERFLAG_MAP | TIMERFLAG_ROUND, []() {
+ ClientPrintAll(HUD_PRINTCONSOLE, "--------------------------------- [Top Defender] ---------------------------------");
+ for (int i = 0; i < sortedPlayers.size(); i++)
+ {
+ ZEPlayer* pPlayer = sortedPlayers[i].Get();
+ if (!pPlayer)
+ continue;
+
+ CCSPlayerController* pController = CCSPlayerController::FromSlot(pPlayer->GetPlayerSlot());
+ if (!pController)
+ continue;
+
+ ClientPrintAll(HUD_PRINTCONSOLE, "%i. %s - %i DMG (%i HITS | %.0f%% HS | %i KILL%s)", i + 1, pController->GetPlayerName(), pPlayer->GetTotalDamage(), pPlayer->GetTotalHits(), ((double)pPlayer->GetTotalHeadshots() / (double)pPlayer->GetTotalHits()) * 100.0f, pPlayer->GetTotalKills(), pPlayer->GetTotalKills() == 1 ? "" : "S");
+ }
+ ClientPrintAll(HUD_PRINTCONSOLE, "----------------------------------------------------------------------------------");
+ return -1.0f;
+ });
+ }
+}
+
+CON_COMMAND_CHAT(tdrank, "[player/rank] - Displays your defender stats or a specified player's stats.")
+{
+ TopDefenderSearch(player, args);
+}
+
+CON_COMMAND_CHAT(tdfind, "[player/rank] - Displays your defender stats or a specified player's stats.")
+{
+ TopDefenderSearch(player, args);
+}
+
+void TopDefenderSearch(CCSPlayerController* player, const CCommand& args)
+{
+ if (!player)
+ {
+ ClientPrint(player, HUD_PRINTCONSOLE, TD_PREFIX "You cannot use this command from the server console.");
+ return;
+ }
+
+ if (sortedPlayers.size() == 0)
+ {
+ ClientPrint(player, HUD_PRINTTALK, TD_PREFIX "There are no top defenders at this time.");
+ return;
+ }
+
+ // First check if no argument is passed
+ if (args.ArgC() < 2)
+ {
+ ZEPlayer* pPlayer = player->GetZEPlayer();
+ if (!pPlayer)
+ return;
+
+ // Search for the player in the sorted array
+ for (int i = 0; i < sortedPlayers.size(); i++)
+ {
+ if (sortedPlayers[i] != pPlayer)
+ continue;
+
+ ClientPrint(player, HUD_PRINTTALK, TD_PREFIX "RANK \4%d \1- \4%d \1DMG (\4%d \1HITS | \4%.0f%% \1HS | \4%d \1KILL%s)", i + 1, pPlayer->GetTotalDamage(), pPlayer->GetTotalHits(), ((double)pPlayer->GetTotalHeadshots() / (double)pPlayer->GetTotalHits()) * 100.0f, pPlayer->GetTotalKills(), pPlayer->GetTotalKills() == 1 ? "" : "S");
+ return;
+ }
+
+ ClientPrint(player, HUD_PRINTTALK, TD_PREFIX "You do not have any stats to display at this time.");
+ }
+ else
+ {
+ // Check if the argument passed is a valid rank
+ // If invalid, we then assume it's a player's name and search accordingly
+ int iRank = Q_atoi(args[1]);
+ if (iRank <= 0 || iRank > sortedPlayers.size())
+ {
+ int iNumClients = 0;
+ int pSlots[MAXPLAYERS];
+
+ if (!g_playerManager->CanTargetPlayers(player, args[1], iNumClients, pSlots, NO_MULTIPLE))
+ return;
+
+ for (int i = 0; i < sortedPlayers.size(); i++)
+ {
+ CCSPlayerController* pController = CCSPlayerController::FromSlot(pSlots[0]);
+ if (!pController)
+ continue;
+
+ ZEPlayer* pTarget = pController->GetZEPlayer();
+ if (!pTarget || sortedPlayers[i] != pTarget)
+ continue;
+
+ ClientPrint(player, HUD_PRINTTALK, TD_PREFIX "RANK \4%d\1: \4%s \1 - \4%d \1DMG (\4%d \1HITS | \4%.0f%% \1HS | \4%d \1KILL%s)", i + 1, pController->GetPlayerName(), pTarget->GetTotalDamage(), pTarget->GetTotalHits(), ((double)pTarget->GetTotalHeadshots() / (double)pTarget->GetTotalHits()) * 100.0f, pTarget->GetTotalKills(), pTarget->GetTotalKills() == 1 ? "" : "S");
+ return;
+ }
+
+ ClientPrint(player, HUD_PRINTTALK, TD_PREFIX "%s has no stats to display at this time.", CCSPlayerController::FromSlot(pSlots[0])->GetPlayerName());
+ }
+ else
+ {
+ ZEPlayer* pPlayer = sortedPlayers[iRank - 1].Get();
+ if (!pPlayer)
+ return;
+
+ CCSPlayerController* pController = CCSPlayerController::FromSlot(pPlayer->GetPlayerSlot());
+ if (!pController)
+ return;
+
+ ClientPrint(player, HUD_PRINTTALK, TD_PREFIX "RANK \4%d\1: \4%s \1- \4%d \1DMG (\4%d \1HITS | \4%.0f%% \1HS | \4%d \1KILL%s)", iRank, pController->GetPlayerName(), pPlayer->GetTotalDamage(), pPlayer->GetTotalHits(), ((double)pPlayer->GetTotalHeadshots() / (double)pPlayer->GetTotalHits()) * 100.0f, pPlayer->GetTotalKills(), pPlayer->GetTotalKills() == 1 ? "" : "S");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/topdefender.h b/src/topdefender.h
new file mode 100644
index 00000000..f9dd7d3a
--- /dev/null
+++ b/src/topdefender.h
@@ -0,0 +1,36 @@
+/**
+ * =============================================================================
+ * CS2Fixes
+ * Copyright (C) 2023-2025 Source2ZE
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * This program 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 the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ */
+
+#pragma once
+
+#include "commands.h"
+
+#define TD_PREFIX " \4[TopDefender]\1 "
+
+extern CConVar g_cvarEnableTopDefender;
+extern CConVar g_cvarTopDefenderChatTag;
+extern CConVar g_cvarTopDefenderNameColor;
+extern CConVar g_cvarTopDefenderChatColor;
+
+void TD_OnPlayerHurt(IGameEvent* pEvent);
+void TD_OnPlayerDeath(IGameEvent* pEvent);
+void TD_OnRoundStart(IGameEvent* pEvent);
+void TD_OnRoundEnd(IGameEvent* pEvent);
+
+void TopDefenderSearch(CCSPlayerController* player, const CCommand& args);
\ No newline at end of file