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