diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 989219ea8..81aed9c34 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -61,11 +61,14 @@ jobs:
run: |
cd ambuild && python setup.py install && cd ..
- - name: Install Clang 16
+ - name: Install Clang 21
if: matrix.os == 'ubuntu-latest'
run: |
- apt update && apt install -y clang-16
- ln -sf /usr/bin/clang-16 /usr/bin/clang && ln -sf /usr/bin/clang++-16 /usr/bin/clang++
+ echo "deb http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-21 main" >> /etc/apt/sources.list.d/llvm.list
+ echo "deb-src http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-21 main" >> /etc/apt/sources.list.d/llvm.list
+ wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
+ apt update && apt install -y clang-21
+ ln -sf /usr/bin/clang-21 /usr/bin/clang && ln -sf /usr/bin/clang++-21 /usr/bin/clang++
- name: Build
working-directory: CS2Fixes
diff --git a/AMBuilder b/AMBuilder
index d5f527955..64ed4706b 100644
--- a/AMBuilder
+++ b/AMBuilder
@@ -49,6 +49,7 @@ for sdk_target in MMSPlugin.sdk_targets:
'src/utils/entity.cpp',
'src/utils/weapon.cpp',
'src/utils/hud_manager.cpp',
+ 'src/utils/utils.cpp',
'src/cs2_sdk/entity/services.cpp',
'src/cs2_sdk/entity/ccsplayerpawn.cpp',
'src/cs2_sdk/entity/cbasemodelentity.cpp',
@@ -69,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 e442bc9e1..466bd0d3b 100644
--- a/CS2Fixes.vcxproj
+++ b/CS2Fixes.vcxproj
@@ -207,6 +207,7 @@
+
@@ -218,6 +219,7 @@
+
@@ -281,6 +283,7 @@
+
@@ -296,6 +299,7 @@
+
diff --git a/CS2Fixes.vcxproj.filters b/CS2Fixes.vcxproj.filters
index d19d113dc..b7d41e59c 100644
--- a/CS2Fixes.vcxproj.filters
+++ b/CS2Fixes.vcxproj.filters
@@ -185,6 +185,9 @@
Source Files\utils
+
+ Source Files\utils
+
Source Files\cs2_sdk\entity
@@ -194,6 +197,9 @@
Source Files\cs2_sdk\entity
+
+ Source Files
+
@@ -412,8 +418,14 @@
Header Files\utils
+
+ Header Files\utils
+
Header Files\cs2_sdk\entity
+
+ Header Files
+
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index e50f30feb..1608da652 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,8 +2,11 @@ FROM registry.gitlab.steamos.cloud/steamrt/sniper/sdk
WORKDIR /app
-RUN apt update && apt install -y clang-16
-RUN ln -sf /usr/bin/clang-16 /usr/bin/clang && ln -sf /usr/bin/clang++-16 /usr/bin/clang++
+RUN echo "deb http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-21 main" >> /etc/apt/sources.list.d/llvm.list
+RUN echo "deb-src http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-21 main" >> /etc/apt/sources.list.d/llvm.list
+RUN wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
+RUN apt update && apt install -y clang-21
+RUN ln -sf /usr/bin/clang-21 /usr/bin/clang && ln -sf /usr/bin/clang++-21 /usr/bin/clang++
RUN git clone https://github.com/alliedmodders/ambuild
RUN cd ambuild && python setup.py install && cd ..
RUN git clone https://github.com/alliedmodders/metamod-source
diff --git a/cfg/cs2fixes/cs2fixes.cfg b/cfg/cs2fixes/cs2fixes.cfg
index 63ecde976..a572988e8 100644
--- a/cfg/cs2fixes/cs2fixes.cfg
+++ b/cfg/cs2fixes/cs2fixes.cfg
@@ -1,12 +1,11 @@
// Feature cvars
cs2f_commands_enable 0 // Whether to enable chat commands
cs2f_admin_commands_enable 0 // Whether to enable admin chat commands
-cs2f_admin_immunity 0 // Mode for which admin immunity system targetting allows: 0 - strictly lower, 1 - equal to or lower, 2 - ignore immunity levels
+cs2f_admin_immunity 0 // Mode for which admin immunity system targetting allows: 0 - strictly lower, 1 - equal to or lower, 2 - ignore immunity levels
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)
@@ -27,10 +26,10 @@ cs2f_fix_game_bans 0 // Whether to fix CS2 game bans spreading to all new jo
cs2f_free_armor 0 // Whether kevlar (1+) and/or helmet (2) are given automatically
cs2f_beacon_particle "particles/cs2fixes/admin_beacon.vpcf" // .vpcf file to be precached and used for player beacon
-
+cs2f_motd_url "" // Server MOTD URL, shows up as a "Server Website" button in scoreboard
cs2f_delay_auth_fail_kick 0 // How long in seconds to delay kicking players when their Steam authentication fails, use with sv_steamauth_enforce 0
-//Screen Shake settings
+// Screen Shake settings
cs2f_noshake_enable 0 // Whether to enable noshake command
cs2f_maximum_shake_amplitude -1 // Shaking Amplitude bigger than this will be clamped (0-16.0), -1 = no clamp
@@ -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
@@ -82,6 +90,7 @@ cs2f_rtv_endround 0 // Whether to immediately end the round when RTV succee
// Map vote settings
cs2f_vote_maps_cooldown 6.0 // Default number of hours until a map can be played again i.e. cooldown
+cs2f_vote_maps_cooldown_rng 0.0 // Randomness range in both directions to apply to map cooldowns
cs2f_vote_max_nominations 10 // Number of nominations to include per vote, out of a maximum of 10
cs2f_vote_max_maps 10 // Number of total maps to include per vote, including nominations, out of a maximum of 10
@@ -122,16 +131,16 @@ cs2f_leader_actions_ct_only 1 // Whether to allow leader actions (like !beacon
cs2f_leader_marker_ct_only 1 // Whether to have zombie leaders' player_pings spawn in particle markers or not
cs2f_leader_mute_player_pings 1 // Whether to mute player pings made by non-leaders
cs2f_leader_model_path "" // Path to player model to be used for leaders
-cs2f_leader_defend_particle "particles/cs2fixes/leader_defend_mark.vpcf" // Path to defend particle to be used with c_defend
-cs2f_leader_mark_particle "particles/cs2fixes/leader_defend_mark.vpcf" // Path to particle to be used when a ct leader using player_ping
-cs2f_leader_can_target_players 0 // Whether a leader can target other players with leader commands (not including c_leader)
-cs2f_leader_vote_multiple 1 // If 1, players can vote up to cs2f_max_leaders leaders. If 0, they may vote for a single leader
+cs2f_leader_defend_particle "particles/cs2fixes/leader_defend_mark.vpcf" // Path to defend particle to be used with c_defend
+cs2f_leader_mark_particle "particles/cs2fixes/leader_defend_mark.vpcf" // Path to particle to be used when a ct leader using player_ping
+cs2f_leader_can_target_players 0 // Whether a leader can target other players with leader commands (not including c_leader)
+cs2f_leader_vote_multiple 1 // If 1, players can vote up to cs2f_max_leaders leaders. If 0, they may vote for a single leader
cs2f_leader_extra_score 20000 // Extra score to give a leader to affect their position on the scoreboard
-cs2f_leader_max_leaders 3 // Max amount of leaders set via c_vl or a leader using c_leader (doesn't impact admins)
-cs2f_leader_max_markers 6 // Max amount of markers set by leaders (doesn't impact admins)
-cs2f_leader_max_glows 3 // Max amount of glows set by leaders (doesn't impact admins)
-cs2f_leader_max_tracers 3 // Max amount of tracers set by leaders (doesn't impact admins)
-cs2f_leader_max_beacons 3 // Max amount of beacons set by leaders (doesn't impact admins)
+cs2f_leader_max_leaders 3 // Max amount of leaders set via c_vl or a leader using c_leader (doesn't impact admins)
+cs2f_leader_max_markers 6 // Max amount of markers set by leaders (doesn't impact admins)
+cs2f_leader_max_glows 3 // Max amount of glows set by leaders (doesn't impact admins)
+cs2f_leader_max_tracers 3 // Max amount of tracers set by leaders (doesn't impact admins)
+cs2f_leader_max_beacons 3 // Max amount of beacons set by leaders (doesn't impact admins)
// Idle Kick Settings
cs2f_idle_kick_time 0.0 // Amount of minutes before kicking idle players. 0 to disable afk kicking.
diff --git a/sdk b/sdk
index 3aa193690..3bb772f7c 160000
--- a/sdk
+++ b/sdk
@@ -1 +1 @@
-Subproject commit 3aa1936902b9730341f6f785999d51727cbcf394
+Subproject commit 3bb772f7c0652cd78089522ac58235b9b34f71d6
diff --git a/src/adminsystem.cpp b/src/adminsystem.cpp
index acc950504..fb358cf9f 100644
--- a/src/adminsystem.cpp
+++ b/src/adminsystem.cpp
@@ -594,11 +594,9 @@ CON_COMMAND_CHAT_FLAGS(rcon, " - Send a command to server console", ADM
}
// Normally this should be done on plugin init, but for whatever reason "log_flags +donotecho" crashes AFTER this
- ExecuteOnce
- (
+ ExecuteOnce(
LoggingSystem_RegisterBackdoorLoggingListener(&g_LoggingListener);
- LoggingSystem_EnableBackdoorLoggingListeners(true);
- );
+ LoggingSystem_EnableBackdoorLoggingListeners(true););
// We don't have the equivalent of ServerExecute (to immediately execute commands) in source2, so manually find and execute the command
diff --git a/src/commands.cpp b/src/commands.cpp
index 159d3b4f0..30b882571 100644
--- a/src/commands.cpp
+++ b/src/commands.cpp
@@ -744,6 +744,12 @@ CON_COMMAND_CHAT(showteam, " - Get a player's current team")
}
}
+// Because sv_fullupdate doesn't work
+CON_COMMAND_F(cs2f_fullupdate, "- Force a full update for all clients.", FCVAR_LINKED_CONCOMMAND | FCVAR_SPONLY)
+{
+ g_playerManager->FullUpdateAllClients();
+}
+
#if _DEBUG
CON_COMMAND_CHAT(myuid, "- Test")
{
diff --git a/src/common.h b/src/common.h
index 327a4605c..c9a25fd2f 100644
--- a/src/common.h
+++ b/src/common.h
@@ -20,6 +20,7 @@
#pragma once
#include "platform.h"
+#include "utils.h"
#define VPROF_LEVEL 1
@@ -44,7 +45,4 @@
#endif
void UnlockConVars();
-void UnlockConCommands();
-
-void Message(const char*, ...);
-void Panic(const char*, ...);
\ No newline at end of file
+void UnlockConCommands();
\ No newline at end of file
diff --git a/src/cs2_sdk/entity/cbaseplayercontroller.h b/src/cs2_sdk/entity/cbaseplayercontroller.h
index deb085aee..a60aa4b64 100644
--- a/src/cs2_sdk/entity/cbaseplayercontroller.h
+++ b/src/cs2_sdk/entity/cbaseplayercontroller.h
@@ -41,7 +41,7 @@ class CBasePlayerController : public CBaseEntity
SCHEMA_FIELD(uint64, m_steamID)
SCHEMA_FIELD(CHandle, m_hPawn)
- SCHEMA_FIELD_POINTER(char, m_iszPlayerName)
+ SCHEMA_FIELD_POINTER(char, m_iszPlayerName) // char m_iszPlayerName[128]
SCHEMA_FIELD(PlayerConnectedState, m_iConnected)
SCHEMA_FIELD(bool, m_bIsHLTV)
SCHEMA_FIELD(uint, m_iDesiredFOV)
diff --git a/src/cs2_sdk/entity/cbaseplayerpawn.h b/src/cs2_sdk/entity/cbaseplayerpawn.h
index 55abdf054..fd46d1b87 100644
--- a/src/cs2_sdk/entity/cbaseplayerpawn.h
+++ b/src/cs2_sdk/entity/cbaseplayerpawn.h
@@ -62,8 +62,19 @@ class CBasePlayerPawn : public CBaseModelEntity
}
}
+ Vector pos = GetEyePosition();
for (CBasePlayerWeapon* pWeapon : vecWeaponsToDrop)
+ {
m_pWeaponServices()->DropWeapon(pWeapon);
+
+ CHandle hWep = pWeapon->GetHandle();
+ CTimer::Create(0.0, TIMERFLAG_MAP | TIMERFLAG_ROUND, [hWep, pos] {
+ CBasePlayerWeapon* pWep = hWep.Get();
+ if (pWep)
+ pWep->Teleport(&pos, nullptr, nullptr);
+ return -1.0f;
+ });
+ }
}
void CommitSuicide(bool bExplode, bool bForce)
diff --git a/src/cs2_sdk/entity/ccsplayercontroller.h b/src/cs2_sdk/entity/ccsplayercontroller.h
index fe01a6eca..8b8613660 100644
--- a/src/cs2_sdk/entity/ccsplayercontroller.h
+++ b/src/cs2_sdk/entity/ccsplayercontroller.h
@@ -33,7 +33,6 @@ class CCSPlayerController : public CBasePlayerController
SCHEMA_FIELD(CCSPlayerController_ActionTrackingServices*, m_pActionTrackingServices)
SCHEMA_FIELD(uint32_t, m_iPing)
SCHEMA_FIELD(CUtlSymbolLarge, m_szClan)
- SCHEMA_FIELD_POINTER(char, m_szClanName) // char m_szClanName[32]
SCHEMA_FIELD(bool, m_bEverFullyConnected)
SCHEMA_FIELD(bool, m_bPawnIsAlive)
SCHEMA_FIELD(int32_t, m_nDisconnectionTick)
@@ -47,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)
{
@@ -144,4 +147,24 @@ class CCSPlayerController : public CBasePlayerController
{
m_iScore() = m_iScore() + iScore;
}
+
+ void SetClanTag(const char* pszClanTag)
+ {
+ // Skip if clan tag is unchanged, since name swap trick has a bit of overhead
+ if (!V_strcmp(m_szClan().String(), pszClanTag))
+ return;
+
+ m_szClan = g_pEntitySystem->AllocPooledString(pszClanTag);
+
+ // This name swap trick is necessary to get clients to display the new clan tag
+ std::string strName = GetPlayerName();
+
+ if (!strName.empty() && strName.back() == ' ')
+ strName.pop_back();
+ else
+ strName.push_back(' ');
+
+ V_strncpy(m_iszPlayerName, strName.c_str(), 128);
+ m_iszPlayerName.NetworkStateChanged();
+ }
};
\ No newline at end of file
diff --git a/src/cs2_sdk/entity/globaltypes.h b/src/cs2_sdk/entity/globaltypes.h
index 7ad24dd57..f6a078cb1 100644
--- a/src/cs2_sdk/entity/globaltypes.h
+++ b/src/cs2_sdk/entity/globaltypes.h
@@ -152,9 +152,9 @@ struct EmitSound_t
{
}
const char* m_pSoundName; // 0x0
- Vector m_vecSoundOrigin; // 0x8
- float m_flVolume; // 0x14
- float m_flSoundTime; // 0x18
+ Vector m_vecSoundOrigin; // 0x8
+ float m_flVolume; // 0x14
+ float m_flSoundTime; // 0x18
private:
uint8_t pad_1c[0x4];
diff --git a/src/cs2fixes.cpp b/src/cs2fixes.cpp
index 4625da83c..a50738420 100644
--- a/src/cs2fixes.cpp
+++ b/src/cs2fixes.cpp
@@ -1,4 +1,4 @@
-/**
+/**
* =============================================================================
* CS2Fixes
* Copyright (C) 2023-2025 Source2ZE
@@ -63,32 +63,6 @@
#include "tier0/memdbgon.h"
-void Message(const char* msg, ...)
-{
- va_list args;
- va_start(args, msg);
-
- char buf[1024] = {};
- V_vsnprintf(buf, sizeof(buf) - 1, msg, args);
-
- ConColorMsg(Color(255, 0, 255, 255), "[CS2Fixes] %s", buf);
-
- va_end(args);
-}
-
-void Panic(const char* msg, ...)
-{
- va_list args;
- va_start(args, msg);
-
- char buf[1024] = {};
- V_vsnprintf(buf, sizeof(buf) - 1, msg, args);
-
- Warning("[CS2Fixes] %s", buf);
-
- va_end(args);
-}
-
class GameSessionConfiguration_t
{};
@@ -625,6 +599,8 @@ void CS2Fixes::Hook_DispatchConCommand(ConCommandRef cmdHandle, const CCommandCo
RETURN_META(MRES_IGNORED);
}
+CConVar g_cvarMotdUrl("cs2f_motd_url", FCVAR_NONE, "Server MOTD URL, shows up as a \"Server Website\" button in scoreboard", "");
+
void CS2Fixes::Hook_StartupServer(const GameSessionConfiguration_t& config, ISource2WorldSession* pSession, const char* pszMapName)
{
g_pEntitySystem = GameEntitySystem();
@@ -644,8 +620,18 @@ void CS2Fixes::Hook_StartupServer(const GameSessionConfiguration_t& config, ISou
g_pPanoramaVoteHandler->Reset();
g_pVoteManager->VoteManager_Init();
-
g_pIdleSystem->Reset();
+
+ INetworkStringTable* pInfoPanelTable = g_pNetworkStringTableServer->FindTable("InfoPanel");
+
+ if (pInfoPanelTable && V_strcmp(g_cvarMotdUrl.Get(), ""))
+ {
+ SetStringUserDataRequest_t pUserData;
+ pUserData.m_pRawData = (void*)g_cvarMotdUrl.Get().Get();
+ pUserData.m_cbDataSize = g_cvarMotdUrl.Get().Length() + 1;
+
+ pInfoPanelTable->AddString(true, "motd", &pUserData);
+ }
}
class CGamePlayerEquip;
@@ -691,11 +677,6 @@ void CS2Fixes::Hook_GameServerSteamAPIDeactivated()
RETURN_META(MRES_IGNORED);
}
-uint32 GetSoundEventHash(const char* pszSoundEventName)
-{
- return MurmurHash2LowerCase(pszSoundEventName, 0x53524332);
-}
-
void CS2Fixes::Hook_PostEvent(CSplitScreenSlot nSlot, bool bLocalOnly, int nClientCount, const uint64* clients,
INetworkMessageInternal* pEvent, const CNetMessage* pData, unsigned long nSize, NetChannelBufType_t bufType)
{
@@ -839,42 +820,6 @@ void CS2Fixes::AllPluginsLoaded()
Message("AllPluginsLoaded\n");
}
-CUtlVector* GetClientList()
-{
- if (!GetNetworkGameServer())
- return nullptr;
-
- static int offset = g_GameConfig->GetOffset("CNetworkGameServer_ClientList");
- return (CUtlVector*)(&GetNetworkGameServer()[offset]);
-}
-
-CServerSideClient* GetClientBySlot(CPlayerSlot slot)
-{
- CUtlVector* pClients = GetClientList();
-
- if (!pClients)
- return nullptr;
-
- return pClients->Element(slot.Get());
-}
-
-void FullUpdateAllClients()
-{
- auto pClients = GetClientList();
-
- if (!pClients)
- return;
-
- FOR_EACH_VEC(*pClients, i)
- (*pClients)[i]->ForceFullUpdate();
-}
-
-// Because sv_fullupdate doesn't work
-CON_COMMAND_F(cs2f_fullupdate, "- Force a full update for all clients.", FCVAR_LINKED_CONCOMMAND | FCVAR_SPONLY)
-{
- FullUpdateAllClients();
-}
-
void CS2Fixes::Hook_ClientActive(CPlayerSlot slot, bool bLoadGame, const char* pszName, uint64 xuid)
{
Message("Hook_ClientActive(%d, %d, \"%s\", %lli)\n", slot, bLoadGame, pszName, xuid);
diff --git a/src/cs2fixes.h b/src/cs2fixes.h
index b9c17ff06..f340b92e8 100644
--- a/src/cs2fixes.h
+++ b/src/cs2fixes.h
@@ -1,4 +1,4 @@
-/**
+/**
* =============================================================================
* CS2Fixes
* Copyright (C) 2023-2025 Source2ZE
@@ -52,11 +52,8 @@ extern CSteamGameServerAPIContext g_steamAPI;
extern CCSGameRules* g_pGameRules;
extern CSpawnGroupMgrGameSystem* g_pSpawnGroupMgr;
extern double g_flUniversalTime;
+extern INetworkGameServer* GetNetworkGameServer();
extern CGlobalVars* GetGlobals();
-extern uint32 GetSoundEventHash(const char* pszSoundEventName);
-extern CUtlVector* GetClientList();
-extern CServerSideClient* GetClientBySlot(CPlayerSlot slot);
-extern void FullUpdateAllClients();
extern CConVar g_cvarDropMapWeapons;
class CS2Fixes : public ISmmPlugin, public IMetamodListener, public ICS2Fixes
diff --git a/src/entwatch.cpp b/src/entwatch.cpp
index 8cc83a004..51da6c44e 100644
--- a/src/entwatch.cpp
+++ b/src/entwatch.cpp
@@ -797,15 +797,14 @@ void EWItemInstance::Pickup(int slot)
if (bShouldSetClantag)
{
+ // Hud tick function sets it
bHasThisClantag = true;
- pController->m_szClan(sClantag);
+
if (g_cvarItemHolderScore.Get() > -1)
{
int score = pController->m_iScore + g_cvarItemHolderScore.Get();
pController->m_iScore = score;
}
-
- EW_UpdateClientClanTags();
}
}
@@ -838,9 +837,8 @@ void EWItemInstance::Drop(EWDropReason reason, CCSPlayerController* pController)
{
if (g_pEWHandler->vecItems[otherItem]->bShowHud && !g_pEWHandler->vecItems[otherItem]->bHasThisClantag)
{
- // Player IS holding another item, score doesnt need adjusting
+ // Player IS holding another item
- pController->m_szClan(g_pEWHandler->vecItems[otherItem]->sClantag);
g_pEWHandler->vecItems[otherItem]->bHasThisClantag = true;
bSetAnotherClantag = true;
break;
@@ -849,18 +847,14 @@ void EWItemInstance::Drop(EWDropReason reason, CCSPlayerController* pController)
}
bHasThisClantag = false;
- if (!bSetAnotherClantag)
+ if (g_cvarItemHolderScore.Get() != 0)
{
- if (g_cvarItemHolderScore.Get() != 0)
- {
- int score = pController->m_iScore - g_cvarItemHolderScore.Get();
- pController->m_iScore = score;
- }
-
- pController->m_szClan("");
+ int score = pController->m_iScore - g_cvarItemHolderScore.Get();
+ pController->m_iScore = score;
}
- EW_UpdateClientClanTags();
+ if (!bSetAnotherClantag)
+ pController->SetClanTag("");
}
char sPlayerInfo[64];
@@ -875,7 +869,7 @@ void EWItemInstance::Drop(EWDropReason reason, CCSPlayerController* pController)
Message(EW_PREFIX "%s has dropped %s (weaponid:%d)\n", sPlayerInfo, szItemName.c_str(), iWeaponEnt);
if (bShowPickup)
- ClientPrintAll(HUD_PRINTTALK, EW_PREFIX "\x03%s \x05has dropped %s%s", pController->GetPlayerName(), sChatColor, szItemName.c_str());
+ ClientPrintAll(HUD_PRINTTALK, EW_PREFIX "\x03%s\x05 has dropped %s%s", pController->GetPlayerName(), sChatColor, szItemName.c_str());
break;
case EWDropReason::Infected:
if (bAllowDrop)
@@ -1213,6 +1207,7 @@ void CEWHandler::PrintLoadedConfig(CPlayerSlot slot)
void CEWHandler::ClearItems()
{
+ vecActiveTransfers.clear();
mapTransfers.clear();
vecItems.clear();
}
@@ -1504,10 +1499,8 @@ void CEWHandler::ResetAllClantags()
pController->m_iScore = score;
}
- pController->m_szClan("");
+ pController->SetClanTag("");
}
-
- EW_UpdateClientClanTags();
}
int CEWHandler::RegisterItem(CBasePlayerWeapon* pWeapon)
@@ -1525,8 +1518,6 @@ int CEWHandler::RegisterItem(CBasePlayerWeapon* pWeapon)
std::shared_ptr instance = std::make_shared(pWeapon->entindex(), item);
- V_snprintf(instance->sClantag, sizeof(EWItemInstance::sClantag), "[+]%s:", instance->szShortName.c_str());
-
bool bKnife = pWeapon->GetWeaponVData()->m_GearSlot() == GEAR_SLOT_KNIFE;
instance->bAllowDrop = !bKnife;
@@ -1618,13 +1609,10 @@ void CEWHandler::PlayerPickup(CCSPlayerPawn* pPawn, int iItemInstance)
item->Pickup(pPawn->m_hOriginalController->GetPlayerSlot());
- if (g_cvarEnableEntwatchHud.Get() && !m_bHudTicking)
- {
- m_bHudTicking = true;
- CTimer::Create(EW_HUD_TICKRATE, TIMERFLAG_MAP | TIMERFLAG_ROUND, [] {
+ if (m_pHudTimer.expired())
+ m_pHudTimer = CTimer::Create(EW_HUD_TICKRATE, TIMERFLAG_MAP | TIMERFLAG_ROUND, [] {
return EW_UpdateHud();
});
- }
}
void CEWHandler::PlayerDrop(EWDropReason reason, int iItemInstance, CCSPlayerController* pController)
@@ -1741,6 +1729,31 @@ void CEWHandler::Transfer(CCSPlayerController* pCaller, int iItemInstance, CHand
}
}
+ // Add this weapon and receiver to active transfers to prevent others from picking it up
+ if (GetGlobals())
+ {
+ std::shared_ptr transfer = std::make_shared();
+ transfer->hReceiver = hReceiver;
+ transfer->hWeapon = pItemWeapon->GetHandle();
+ transfer->flTime = GetGlobals()->curtime + 0.95;
+ vecActiveTransfers.push_back(transfer);
+ CTimer::Create(1.0, TIMERFLAG_MAP | TIMERFLAG_ROUND, [] {
+ if (!GetGlobals())
+ return -1.0f;
+
+ for (int i = 0; i < g_pEWHandler->vecActiveTransfers.size(); i++)
+ {
+ std::shared_ptr transfer = g_pEWHandler->vecActiveTransfers[i];
+ if (transfer->flTime < GetGlobals()->curtime)
+ {
+ g_pEWHandler->vecActiveTransfers.erase(g_pEWHandler->vecActiveTransfers.begin() + i);
+ i--;
+ }
+ }
+ return -1.0f;
+ });
+ }
+
// Give the item to the receiver
Vector vecOrigin = pReceiverPawn->GetAbsOrigin();
pItemWeapon->Teleport(&vecOrigin, nullptr, nullptr);
@@ -1974,8 +1987,17 @@ float EW_UpdateHud()
std::string sItemText = pItem->GetHandlerStateText();
- // TODO: std::format not supported in clang16 by default
- // sHudText.append(std::format("\n[{}]{}: {}", sItemText, pItem->szShortName, pOwner->GetPlayerName()));
+ if (g_cvarUseEntwatchClantag.Get())
+ {
+ V_snprintf(pItem->sClantag, sizeof(EWItemInstance::sClantag), "[%s]%s:", sItemText.c_str(), pItem->szShortName.c_str());
+ if (pItem->bHasThisClantag)
+ pOwner->SetClanTag(pItem->sClantag);
+ }
+
+ if (!g_cvarEnableEntwatchHud.Get())
+ continue;
+
+ // std::format not supported in clang16 by default (steamrt3)
if (!bFirst)
{
sHudText.append("\n");
@@ -2051,7 +2073,6 @@ void EW_RoundPreStart()
g_pEWHandler->ResetAllClantags();
g_pEWHandler->ClearItems();
- g_pEWHandler->m_bHudTicking = false;
}
void EW_OnEntitySpawned(CEntityInstance* pEntity)
@@ -2155,6 +2176,27 @@ bool EW_Detour_CCSPlayer_WeaponServices_CanUse(CCSPlayer_WeaponServices* pWeapon
if (!zpPlayer || zpPlayer->IsEbanned())
return false;
+ // Limit any active transfers to the receiver only
+ if (g_pEWHandler->vecActiveTransfers.size() == 0)
+ return true;
+
+ for (int i = 0; i < g_pEWHandler->vecActiveTransfers.size(); i++)
+ {
+ std::shared_ptr transfer = g_pEWHandler->vecActiveTransfers[i];
+ CHandle hReceiver = transfer->hReceiver;
+ CHandle hWeapon = transfer->hWeapon;
+ if (!hReceiver.Get() || !hWeapon.Get())
+ continue;
+
+ if (hWeapon.Get() != pPlayerWeapon)
+ continue;
+
+ if (hReceiver.Get() == ccsPlayer)
+ return true;
+ else
+ return false;
+ }
+
return true;
}
@@ -2247,6 +2289,14 @@ void EW_PlayerDeathPre(CCSPlayerController* pController)
void EW_PlayerDisconnect(int slot)
{
+ ZEPlayer* pPlayer = g_playerManager->GetPlayer(CPlayerSlot(slot));
+ if (pPlayer)
+ {
+ CPointWorldText* pText = pPlayer->GetEntwatchHud();
+ if (pText)
+ pText->Remove();
+ }
+
auto i = g_pEWHandler->mapTransfers.find(slot);
if (i != g_pEWHandler->mapTransfers.end())
g_pEWHandler->mapTransfers.erase(slot);
@@ -2259,49 +2309,6 @@ void EW_PlayerDisconnect(int slot)
g_pEWHandler->PlayerDrop(EWDropReason::Disconnect, -1, pController);
}
-// An event needs to be sent to force clients to see up to date clantags
-void EW_UpdateClientClanTags()
-{
- // Cannot send this event during map vote, as it breaks voting client side
- if (!GetGlobals() || !g_pMapVoteSystem->IsIntermissionAllowed())
- return;
-
- static IGameEvent* pEvent = nullptr;
-
- if (!pEvent)
- pEvent = g_gameEventManager->CreateEvent("nextlevel_changed");
-
- if (!pEvent)
- {
- Panic("Failed to create nextlevel_changed event\n");
- return;
- }
-
- INetworkMessageInternal* pMsg = g_pNetworkMessages->FindNetworkMessageById(GE_Source1LegacyGameEvent);
- if (!pMsg)
- {
- Panic("Failed to create Source1LegacyGameEvent\n");
- return;
- }
- CNetMessagePB* data = pMsg->AllocateMessage()->ToPB();
- g_gameEventManager->SerializeEvent(pEvent, data);
-
- CRecipientFilter filter;
- for (int i = 0; i < GetGlobals()->maxClients; i++)
- {
- ZEPlayer* pPlayer = g_playerManager->GetPlayer(i);
-
- if (!pPlayer || pPlayer->IsFakeClient() || !pPlayer->IsAuthenticated())
- continue;
-
- if (pPlayer->GetEntwatchClangtags())
- filter.AddRecipient(pPlayer->GetPlayerSlot());
- }
-
- g_gameEventSystem->PostEventAbstract(-1, false, &filter, pMsg, data, 0);
- delete data;
-}
-
bool EW_IsFireOutputHooked()
{
return std::any_of(mapIOFunctions.begin(), mapIOFunctions.end(), [](const auto& p) { return p.first == "entwatch"; });
@@ -2650,34 +2657,6 @@ CON_COMMAND_CHAT(ew_dump, "- Prints the currently loaded config to console")
g_pEWHandler->PrintLoadedConfig(player->GetPlayerSlot());
}
-CON_COMMAND_CHAT(etag, "- Toggle EntWatch clantags on the scoreboard")
-{
- if (!g_cvarEnableEntWatch.Get())
- return;
-
- if (!player)
- {
- ClientPrint(player, HUD_PRINTTALK, EW_PREFIX "Only usable in game.");
- return;
- }
-
- ZEPlayer* zpPlayer = g_playerManager->GetPlayer(player->GetPlayerSlot());
- if (!zpPlayer)
- {
- ClientPrint(player, HUD_PRINTTALK, EW_PREFIX "Something went wrong, try again later.");
- return;
- }
-
- bool bCurrentStatus = zpPlayer->GetEntwatchClangtags();
- bCurrentStatus = !bCurrentStatus;
- zpPlayer->SetEntwatchClangtags(bCurrentStatus);
-
- if (bCurrentStatus)
- ClientPrint(player, HUD_PRINTTALK, EW_PREFIX "You have\x04 Enabled\x01 EntWatch clantag updates");
- else
- ClientPrint(player, HUD_PRINTTALK, EW_PREFIX "You have\x07 Disabled\x01 EntWatch clantag updates");
-}
-
CON_COMMAND_CHAT(hud, "- Toggle EntWatch HUD")
{
if (!g_cvarEnableEntWatch.Get())
diff --git a/src/entwatch.h b/src/entwatch.h
index 42ba3cd38..981751675 100644
--- a/src/entwatch.h
+++ b/src/entwatch.h
@@ -36,7 +36,6 @@ extern CConVar g_cvarEnableEntwatchHud;
#define EW_PREFIX " \4[EntWatch]\1 "
#define EW_PREF_HUD_MODE "entwatch_hud"
-#define EW_PREF_CLANTAG "entwatch_clantag"
#define EW_PREF_HUDPOS_X "entwatch_hudpos_x"
#define EW_PREF_HUDPOS_Y "entwatch_hudpos_y"
#define EW_PREF_HUDCOLOR "entwatch_hudcolor"
@@ -206,13 +205,19 @@ struct ETransferInfo
float flTime; // The time when the command was initiated
};
+struct EActiveTransfer
+{
+ CHandle hReceiver;
+ CHandle hWeapon;
+ float flTime;
+};
+
class CEWHandler
{
public:
CEWHandler()
{
bConfigLoaded = false;
- m_bHudTicking = false;
iBaseBtnUseHookId = -1;
iPhysboxUseHookId = -1;
@@ -279,9 +284,10 @@ class CEWHandler
int iRotBtnUseHookId;
int iMomRotBtnUseHookId;
- bool m_bHudTicking;
+ std::weak_ptr m_pHudTimer;
- std::map> mapTransfers; // Any etransfers that target multiple items
+ std::map> mapTransfers; // Any etransfers that target multiple items
+ std::vector> vecActiveTransfers; // Active transfers where only the receiver can pickup the weapon
};
extern CEWHandler* g_pEWHandler;
@@ -297,7 +303,6 @@ void EW_DropWeapon(CCSPlayer_WeaponServices* pWeaponServices, CBasePlayerWeapon*
void EW_PlayerDeath(IGameEvent* pEvent);
void EW_PlayerDeathPre(CCSPlayerController* pController);
void EW_PlayerDisconnect(int slot);
-void EW_UpdateClientClanTags();
bool EW_IsFireOutputHooked();
void EW_FireOutput(const CEntityIOOutput* pThis, CEntityInstance* pActivator, CEntityInstance* pCaller, const CVariant* value, float flDelay);
int GetTemplateSuffixNumber(const char* szName);
diff --git a/src/events.cpp b/src/events.cpp
index 23b1e5274..ad32f4c51 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/map_votes.cpp b/src/map_votes.cpp
index 3e69452e4..b8a5d1c10 100644
--- a/src/map_votes.cpp
+++ b/src/map_votes.cpp
@@ -40,6 +40,7 @@
CMapVoteSystem* g_pMapVoteSystem = nullptr;
CConVar g_cvarVoteMapsCooldown("cs2f_vote_maps_cooldown", FCVAR_NONE, "Default number of hours until a map can be played again i.e. cooldown", 6.0f);
+CConVar g_cvarVoteMapsCooldownRng("cs2f_vote_maps_cooldown_rng", FCVAR_NONE, "Randomness range in both directions to apply to map cooldowns", 0.0f);
CConVar g_cvarVoteMaxNominations("cs2f_vote_max_nominations", FCVAR_NONE, "Number of nominations to include per vote, out of a maximum of 10", 10, true, 0, true, 10);
CConVar g_cvarVoteMaxMaps("cs2f_vote_max_maps", FCVAR_NONE, "Number of total maps to include per vote, including nominations, out of a maximum of 10", 10, true, 2, true, 10);
@@ -607,7 +608,7 @@ std::vector> CMapVoteSystem::GetMapsFromSubstring(const ch
void CMapVoteSystem::HandlePlayerMapLookup(CCSPlayerController* pController, std::string strMapSubstring, bool bAdmin, QueryCallback_t callbackSuccess)
{
- strMapSubstring = g_pMapVoteSystem->StringToLower(strMapSubstring);
+ strMapSubstring = StringToLower(strMapSubstring);
const char* pszMapSubstring = strMapSubstring.c_str();
auto vecFoundMaps = GetMapsFromSubstring(pszMapSubstring);
@@ -1212,14 +1213,6 @@ std::string CMapVoteSystem::ConvertFloatToString(float fValue, int precision)
return str;
}
-std::string CMapVoteSystem::StringToLower(std::string strValue)
-{
- for (int i = 0; strValue[i]; i++)
- strValue[i] = tolower(strValue[i]);
-
- return strValue;
-}
-
std::string CMapVoteSystem::GetMapCooldownText(const char* pszMapName, bool bPlural)
{
std::shared_ptr pCooldown = GetMapCooldown(pszMapName);
@@ -1270,6 +1263,17 @@ void CMapVoteSystem::PutMapOnCooldown(const char* pszMapName, float fCooldown)
fCooldown = pMap->GetCustomCooldown();
else
fCooldown = g_cvarVoteMapsCooldown.Get();
+
+ // Add randomness if applicable
+ if (g_cvarVoteMapsCooldown.Get() != 0.0f)
+ {
+ float flRandomValue = ((float)rand() / RAND_MAX) * g_cvarVoteMapsCooldownRng.Get();
+
+ if (rand() % 2)
+ fCooldown += flRandomValue;
+ else
+ fCooldown -= flRandomValue;
+ }
}
time_t timeCooldown = std::time(0) + (time_t)(fCooldown * 60 * 60);
diff --git a/src/map_votes.h b/src/map_votes.h
index e80d5bb1d..b3fe7184b 100644
--- a/src/map_votes.h
+++ b/src/map_votes.h
@@ -26,10 +26,10 @@
#include "vendor/nlohmann/json_fwd.hpp"
#include
#include
+#include
#include
#include
#include
-#include
class CMap;
using ordered_json = nlohmann::ordered_json;
@@ -214,7 +214,6 @@ class CMapVoteSystem
void OnLevelShutdown();
std::vector> GetMapCooldowns() { return m_vecCooldowns; }
std::string ConvertFloatToString(float fValue, int precision);
- std::string StringToLower(std::string strValue);
void SetDisabledCooldowns(bool bValue) { g_bDisableCooldowns = bValue; } // Can be used by custom fork features, e.g. an auto-restart
void ProcessGroupCooldowns();
bool ReloadMapList(bool bReloadMap = true);
diff --git a/src/playermanager.cpp b/src/playermanager.cpp
index 7af260f8d..35f4954d4 100644
--- a/src/playermanager.cpp
+++ b/src/playermanager.cpp
@@ -715,12 +715,6 @@ void ZEPlayer::SetEntwatchHudMode(int iMode)
g_pUserPreferencesSystem->SetPreferenceInt(m_slot.Get(), EW_PREF_HUD_MODE, m_iEntwatchHudMode);
}
-void ZEPlayer::SetEntwatchClangtags(bool bStatus)
-{
- m_bEntwatchClantags = bStatus;
- g_pUserPreferencesSystem->SetPreferenceInt(m_slot.Get(), EW_PREF_CLANTAG, bStatus ? 1 : 0);
-}
-
void ZEPlayer::SetEntwatchHudColor(Color colorHud)
{
m_colorEntwatchHud = colorHud;
@@ -1596,7 +1590,13 @@ ETargetError CPlayerManager::GetPlayersFromString(CCSPlayerController* pPlayer,
if (!pTarget || !pTarget->IsController() || !pTarget->IsConnected() || pTarget->m_bIsHLTV)
continue;
- if ((!bExactName && V_stristr(pTarget->GetPlayerName(), pszTarget)) || !V_strcmp(pTarget->GetPlayerName(), pszTarget))
+ std::string strName = pTarget->GetPlayerName();
+
+ // Ignore space that might be added by clan tag name swap trick
+ if (!strName.empty() && strName.back() == ' ')
+ strName.pop_back();
+
+ if ((!bExactName && V_stristr(strName.c_str(), pszTarget)) || !V_strcmp(strName.c_str(), pszTarget))
{
nType = ETargetType::PLAYER;
if (iNumClients == 1)
@@ -1845,4 +1845,15 @@ int CPlayerManager::GetOnlinePlayerCount(bool bCountBots)
}
return iOnlinePlayers;
+}
+
+void CPlayerManager::FullUpdateAllClients()
+{
+ auto pClients = GetClientList();
+
+ if (!pClients)
+ return;
+
+ FOR_EACH_VEC(*pClients, i)
+ (*pClients)[i]->ForceFullUpdate();
}
\ No newline at end of file
diff --git a/src/playermanager.h b/src/playermanager.h
index 5cf1e3204..fd5165d2a 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;
@@ -192,11 +193,11 @@ class ZEPlayer
m_pActiveZRModel = nullptr;
m_iButtonWatchMode = 0;
m_iEntwatchHudMode = 0;
- m_bEntwatchClantags = true;
m_colorEntwatchHud = Color(255, 255, 255, 255);
m_flEntwatchHudX = -7.5f;
m_flEntwatchHudY = -2.0f;
m_flEntwatchHudSize = 60.0f;
+ m_bTopDefender = false;
}
~ZEPlayer()
@@ -231,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; }
@@ -261,12 +263,12 @@ class ZEPlayer
void SetActiveZRClass(std::shared_ptr pZRModel) { m_pActiveZRClass = pZRModel; }
void SetActiveZRModel(std::shared_ptr pZRClass) { m_pActiveZRModel = pZRClass; }
void SetEntwatchHudMode(int iMode);
- void SetEntwatchClangtags(bool bStatus);
void SetPointOrient(CPointOrient* pOrient) { m_hPointOrient.Set(pOrient); }
void SetEntwatchHud(CPointWorldText* pWorldText) { m_hEntwatchHud.Set(pWorldText); }
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; }
@@ -278,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; }
@@ -309,13 +312,13 @@ class ZEPlayer
std::shared_ptr GetActiveZRModel() { return m_pActiveZRModel; }
int GetButtonWatchMode();
int GetEntwatchHudMode();
- bool GetEntwatchClangtags() { return m_bEntwatchClantags; }
CPointOrient* GetPointOrient() { return m_hPointOrient.Get(); }
CPointWorldText* GetEntwatchHud() { return m_hEntwatchHud.Get(); }
Color GetEntwatchHudColor() { return m_colorEntwatchHud; }
float GetEntwatchHudX() { return m_flEntwatchHudX; }
float GetEntwatchHudY() { return m_flEntwatchHudY; }
float GetEntwatchHudSize() { return m_flEntwatchHudSize; }
+ bool GetTopDefenderStatus() { return m_bTopDefender; }
void OnSpawn();
void OnAuthenticated();
@@ -349,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;
@@ -384,11 +388,11 @@ class ZEPlayer
CHandle m_hPointOrient;
CHandle m_hEntwatchHud;
int m_iEntwatchHudMode;
- bool m_bEntwatchClantags;
Color m_colorEntwatchHud;
float m_flEntwatchHudX;
float m_flEntwatchHudY;
float m_flEntwatchHudSize;
+ bool m_bTopDefender;
};
class CPlayerManager
@@ -399,7 +403,7 @@ class CPlayerManager
V_memset(m_vecPlayers, 0, sizeof(m_vecPlayers));
m_nUsingStopSound = -1; // On by default
m_nUsingSilenceSound = 0;
- m_nUsingZSounds = -1; // On by default
+ m_nUsingZSounds = -1; // On by default
m_nUsingStopDecals = -1; // On by default
m_nUsingNoShake = 0;
}
@@ -447,6 +451,7 @@ class CPlayerManager
void UpdatePlayerStates();
int GetOnlinePlayerCount(bool bCountBots);
+ void FullUpdateAllClients();
STEAM_GAMESERVER_CALLBACK_MANUAL(CPlayerManager, OnValidateAuthTicket, ValidateAuthTicketResponse_t, m_CallbackValidateAuthTicketResponse);
diff --git a/src/topdefender.cpp b/src/topdefender.cpp
new file mode 100644
index 000000000..b0091b480
--- /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 000000000..f9dd7d3a3
--- /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
diff --git a/src/user_preferences.cpp b/src/user_preferences.cpp
index 1e32219cf..240723d75 100644
--- a/src/user_preferences.cpp
+++ b/src/user_preferences.cpp
@@ -116,7 +116,6 @@ void CUserPreferencesSystem::OnPutPreferences(int iSlot)
// EntWatch
int iEntwatchMode = GetPreferenceInt(iSlot, EW_PREF_HUD_MODE, 0);
- bool bEntwatchClantag = (bool)GetPreferenceInt(iSlot, EW_PREF_CLANTAG, 1);
float flEntwatchHudposX = GetPreferenceFloat(iSlot, EW_PREF_HUDPOS_X, EW_HUDPOS_X_DEFAULT);
float flEntwatchHudposY = GetPreferenceFloat(iSlot, EW_PREF_HUDPOS_Y, EW_HUDPOS_Y_DEFAULT);
Color ewHudColor;
@@ -137,7 +136,6 @@ void CUserPreferencesSystem::OnPutPreferences(int iSlot)
// Set EntWatch
player->SetEntwatchHudMode(iEntwatchMode);
- player->SetEntwatchClangtags(bEntwatchClantag);
player->SetEntwatchHudPos(flEntwatchHudposX, flEntwatchHudposY);
player->SetEntwatchHudColor(ewHudColor);
player->SetEntwatchHudSize(flEntwatchHudSize);
diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp
new file mode 100644
index 000000000..34953d01f
--- /dev/null
+++ b/src/utils/utils.cpp
@@ -0,0 +1,80 @@
+/**
+ * =============================================================================
+ * 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 "utils.h"
+#include "../cs2fixes.h"
+#include "../gameconfig.h"
+
+void Message(const char* msg, ...)
+{
+ va_list args;
+ va_start(args, msg);
+
+ char buf[1024] = {};
+ V_vsnprintf(buf, sizeof(buf) - 1, msg, args);
+
+ ConColorMsg(Color(255, 0, 255, 255), "[CS2Fixes] %s", buf);
+
+ va_end(args);
+}
+
+void Panic(const char* msg, ...)
+{
+ va_list args;
+ va_start(args, msg);
+
+ char buf[1024] = {};
+ V_vsnprintf(buf, sizeof(buf) - 1, msg, args);
+
+ Warning("[CS2Fixes] %s", buf);
+
+ va_end(args);
+}
+
+CUtlVector* GetClientList()
+{
+ if (!GetNetworkGameServer())
+ return nullptr;
+
+ static int offset = g_GameConfig->GetOffset("CNetworkGameServer_ClientList");
+ return (CUtlVector*)(&GetNetworkGameServer()[offset]);
+}
+
+CServerSideClient* GetClientBySlot(CPlayerSlot slot)
+{
+ CUtlVector* pClients = GetClientList();
+
+ if (!pClients)
+ return nullptr;
+
+ return pClients->Element(slot.Get());
+}
+
+uint32 GetSoundEventHash(const char* pszSoundEventName)
+{
+ return MurmurHash2LowerCase(pszSoundEventName, 0x53524332);
+}
+
+std::string StringToLower(std::string strValue)
+{
+ for (int i = 0; strValue[i]; i++)
+ strValue[i] = tolower(strValue[i]);
+
+ return strValue;
+}
\ No newline at end of file
diff --git a/src/utils/utils.h b/src/utils/utils.h
new file mode 100644
index 000000000..de4c434d0
--- /dev/null
+++ b/src/utils/utils.h
@@ -0,0 +1,35 @@
+/**
+ * =============================================================================
+ * 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 "playerslot.h"
+#include "utlvector.h"
+#include
+
+class CServerSideClient;
+
+void Message(const char*, ...);
+void Panic(const char*, ...);
+
+CUtlVector* GetClientList();
+CServerSideClient* GetClientBySlot(CPlayerSlot slot);
+
+uint32 GetSoundEventHash(const char* pszSoundEventName);
+std::string StringToLower(std::string strValue);
diff --git a/src/zombiereborn.h b/src/zombiereborn.h
index dd3729ecc..19cbb2b1d 100644
--- a/src/zombiereborn.h
+++ b/src/zombiereborn.h
@@ -23,8 +23,8 @@
#include "entity/ccsplayercontroller.h"
#include "entity/ccsplayerpawn.h"
#include "eventlistener.h"
-#include "gamesystem.h"
#include "gameevents.pb.h"
+#include "gamesystem.h"
#include "vendor/nlohmann/json_fwd.hpp"
using ordered_json = nlohmann::ordered_json;