11#include " plugin.h"
2+ #include " regamedll.h"
23
34#ifndef __linux__
45#define __linux__
@@ -21,6 +22,16 @@ Plugin &GetPlugin()
2122 return plugin;
2223}
2324
25+ // Access CS game rules through ReGameDLL API instead of the g_pGameRules extern
26+ // (which lives in the game DLL and is not linkable from our plugin).
27+ static CHalfLifeMultiplay *GetGameRules ()
28+ {
29+ if (g_ReGameApi) {
30+ return static_cast <CHalfLifeMultiplay *>(g_ReGameApi->GetGameRules ());
31+ }
32+ return nullptr ;
33+ }
34+
2435void Plugin::OnMetaAttach ()
2536{
2637 std::srand (static_cast <unsigned >(std::time (nullptr )));
@@ -95,6 +106,12 @@ void Plugin::ForceStartFromServer()
95106
96107void Plugin::OnServerActivate ()
97108{
109+ mpFreezeTimeCvar_ = g_engfuncs.pfnCVarGetPointer (" mp_freezetime" );
110+ mpBuyTimeCvar_ = g_engfuncs.pfnCVarGetPointer (" mp_buytime" );
111+ roundTimeMsgId_ = gpMetaUtilFuncs->pfnGetUserMsgID (&Plugin_info, " RoundTime" , NULL );
112+ if (roundTimeMsgId_ <= 0 ) {
113+ Log (" OnServerActivate: RoundTime message not found\n " );
114+ }
98115 LoadAdmins ();
99116 ResetMatch (true );
100117 SetState (CvarInt (cvars_.enabled ) ? MatchState::Warmup : MatchState::Disabled);
@@ -130,6 +147,12 @@ void Plugin::OnStartFrame()
130147 callback ();
131148 }
132149 }
150+
151+ // Safety: keep the round frozen while paused
152+ if (paused_ && GetGameRules () && !GetGameRules ()->m_bFreezePeriod ) {
153+ GetGameRules ()->m_bFreezePeriod = TRUE ;
154+ GetGameRules ()->m_fRoundStartTime = gpGlobals->time ;
155+ }
133156}
134157
135158bool Plugin::OnClientConnect (edict_t *entity, const char *name)
@@ -359,7 +382,7 @@ void Plugin::LoadAdmins()
359382void Plugin::ResetMatch (bool keepWarmup)
360383{
361384 ClearTasks ();
362- ServerCommand ( " pausable 0 \n " );
385+ CancelTask ( " pause_countdown " );
363386 RestoreKnifeRoundWeapons ();
364387 for (auto &player : players_) {
365388 player.ready = false ;
@@ -378,6 +401,8 @@ void Plugin::ResetMatch(bool keepWarmup)
378401 lastObservedCTScore_ = 0 ;
379402 paused_ = false ;
380403 techPaused_ = false ;
404+ pauseRequested_ = false ;
405+ pauseDuration_ = 0 ;
381406 halftimeScoresSaved_ = false ;
382407 techUnpauseVotes_.clear ();
383408 restarting_ = false ;
@@ -748,65 +773,146 @@ void Plugin::RestartMatch()
748773 StartReady ();
749774}
750775
751- void Plugin::PauseMatch ( )
776+ void Plugin::RequestPause ( const char *caller, int duration, bool isTech )
752777{
753- if (paused_ ) {
754- Broadcast (" [XMP] Match is already paused .\n " );
778+ if (pauseRequested_ ) {
779+ Broadcast (" [XMP] A pause is already queued for the next round .\n " );
755780 return ;
756781 }
757- paused_ = true ;
758- techPaused_ = false ;
782+ if (!IsLiveState (state_)) {
783+ Broadcast (" [XMP] Cannot pause outside of a live match.\n " );
784+ return ;
785+ }
786+ pauseRequested_ = true ;
787+ pauseDuration_ = duration;
788+ techPaused_ = isTech;
759789 techUnpauseVotes_.clear ();
760- Broadcast (" [XMP] Match paused for %.0f seconds.\n " , CvarFloat (cvars_.pauseTime ));
761- ServerCommand (" pausable 1\n " );
762- Schedule (" pause" , CvarFloat (cvars_.pauseTime ), false , [this ]() { UnpauseMatch (); });
790+ if (isTech) {
791+ Broadcast (" [XMP] %s called a technical timeout. Match will pause on next round start — both teams must .unpause to continue.\n " , caller);
792+ } else {
793+ Broadcast (" [XMP] %s paused the match. Pausing on next round start for %d seconds.\n " , caller, duration);
794+ }
795+ }
796+
797+ void Plugin::PauseMatch ()
798+ {
799+ if (paused_ || pauseRequested_) {
800+ Broadcast (" [XMP] Match is already pausing or paused.\n " );
801+ return ;
802+ }
803+ RequestPause (" Admin" , static_cast <int >(CvarFloat (cvars_.pauseTime )), false );
763804}
764805
765806void Plugin::TimeoutMatch ()
766807{
767- if (paused_) {
768- Broadcast (" [XMP] Match is already paused.\n " );
808+ if (paused_ || pauseRequested_ ) {
809+ Broadcast (" [XMP] Match is already pausing or paused.\n " );
769810 return ;
770811 }
771812 if (!IsLiveState (state_)) {
772813 Broadcast (" [XMP] Timeout can only be called during a live match.\n " );
773814 return ;
774815 }
775- paused_ = true ;
776- techPaused_ = false ;
777- techUnpauseVotes_.clear ();
778- Broadcast (" [XMP] Timeout called — match paused for %.0f seconds.\n " , CvarFloat (cvars_.timeoutTime ));
779- ServerCommand (" pausable 1\n " );
780- Schedule (" pause" , CvarFloat (cvars_.timeoutTime ), false , [this ]() { UnpauseMatch (); });
816+ RequestPause (" Timeout" , static_cast <int >(CvarFloat (cvars_.timeoutTime )), false );
781817}
782818
783819void Plugin::TechTimeout ()
784820{
785- if (paused_) {
786- Broadcast (" [XMP] Match is already paused.\n " );
821+ if (paused_ || pauseRequested_ ) {
822+ Broadcast (" [XMP] Match is already pausing or paused.\n " );
787823 return ;
788824 }
789825 if (!IsLiveState (state_)) {
790826 Broadcast (" [XMP] Technical timeout can only be called during a live match.\n " );
791827 return ;
792828 }
829+ RequestPause (" Tech" , static_cast <int >(CvarFloat (cvars_.timeoutTime )), true );
830+ }
831+
832+ void Plugin::ApplyPause ()
833+ {
834+ if (!GetGameRules ()) {
835+ Log (" ApplyPause: CSGameRules is null, cannot pause\n " );
836+ return ;
837+ }
793838 paused_ = true ;
794- techPaused_ = true ;
795- techUnpauseVotes_.clear ();
796- Broadcast (" [XMP] Technical timeout called. Match paused until both teams .unpause.\n " );
797- ServerCommand (" pausable 1\n " );
839+ pauseRequested_ = false ;
840+
841+ if (mpFreezeTimeCvar_) savedFreezeTime_ = mpFreezeTimeCvar_->value ;
842+ if (mpBuyTimeCvar_) savedBuyTime_ = mpBuyTimeCvar_->value ;
843+
844+ // Extend mp_freezetime so the engine keeps the round frozen for the pause duration
845+ if (mpFreezeTimeCvar_) {
846+ g_engfuncs.pfnCvar_DirectSet (mpFreezeTimeCvar_, std::to_string (pauseDuration_).c_str ());
847+ }
848+ if (mpBuyTimeCvar_) {
849+ g_engfuncs.pfnCvar_DirectSet (mpBuyTimeCvar_, std::to_string (pauseDuration_).c_str ());
850+ }
851+
852+ GetGameRules ()->m_bFreezePeriod = TRUE ;
853+ GetGameRules ()->m_fRoundStartTime = gpGlobals->time ;
854+ GetGameRules ()->m_iRoundTimeSecs = pauseDuration_ + 1 ;
855+ GetGameRules ()->m_iIntroRoundTime = pauseDuration_ + 1 ;
856+
857+ // Send RoundTime message so the client HUD reflects the extended timer
858+ if (roundTimeMsgId_ > 0 ) {
859+ g_engfuncs.pfnMessageBegin (MSG_ALL , roundTimeMsgId_, NULL , NULL );
860+ g_engfuncs.pfnWriteShort (static_cast <int >(pauseDuration_ + 1 ));
861+ g_engfuncs.pfnMessageEnd ();
862+ }
863+
864+ // Periodic countdown updates every 15 seconds
865+ if (techPaused_) {
866+ Broadcast (" [XMP] Technical timeout active. Both teams type .unpause to continue.\n " );
867+ } else {
868+ Broadcast (" [XMP] Match paused for %d seconds.\n " , pauseDuration_);
869+ Schedule (" pause" , static_cast <float >(pauseDuration_), false , [this ]() { UnpauseMatch (); });
870+ }
871+
872+ Schedule (" pause_countdown" , 15 .0f , true , [this ]() {
873+ if (!paused_ || !GetGameRules ()) return ;
874+ const float elapsed = gpGlobals->time - GetGameRules ()->m_fRoundStartTime ;
875+ const int remaining = pauseDuration_ - static_cast <int >(elapsed);
876+ if (remaining > 0 && !techPaused_) {
877+ Broadcast (" [XMP] Match unpausing in ~%d seconds.\n " , remaining);
878+ }
879+ });
798880}
799881
800882void Plugin::UnpauseMatch ()
801883{
802884 if (!paused_) {
803885 return ;
804886 }
887+
888+ CancelTask (" pause" );
889+ CancelTask (" pause_countdown" );
890+
891+ // Restore original freezetime
892+ if (mpFreezeTimeCvar_ && savedFreezeTime_ > 0 .0f ) {
893+ char buf[32 ];
894+ snprintf (buf, sizeof (buf), " %.0f" , savedFreezeTime_);
895+ g_engfuncs.pfnCvar_DirectSet (mpFreezeTimeCvar_, buf);
896+ }
897+
898+ // Restore original buytime
899+ if (mpBuyTimeCvar_ && savedBuyTime_ > 0 .0f ) {
900+ char buf[32 ];
901+ snprintf (buf, sizeof (buf), " %.0f" , savedBuyTime_);
902+ g_engfuncs.pfnCvar_DirectSet (mpBuyTimeCvar_, buf);
903+ }
904+
905+ // End freezetime
906+ if (GetGameRules ()) {
907+ GetGameRules ()->m_bFreezePeriod = FALSE ;
908+ }
909+
805910 paused_ = false ;
806911 techPaused_ = false ;
912+ pauseRequested_ = false ;
913+ pauseDuration_ = 0 ;
807914 techUnpauseVotes_.clear ();
808- CancelTask (" pause" );
809- ServerCommand (" pausable 0\n " );
915+
810916 Broadcast (" [XMP] Match unpaused.\n " );
811917}
812918
@@ -1520,8 +1626,16 @@ void Plugin::FinishMatch()
15201626 }
15211627}
15221628
1523- bool Plugin::DispatchCommand (edict_t *entity, const std::string & raw)
1629+ bool Plugin::DispatchCommand (edict_t *entity, std::string raw)
15241630{
1631+ // Trim leading whitespace — some engine builds include it in pfnCmd_Args()
1632+ const auto first = raw.find_first_not_of (" \t\r\n\" " );
1633+ if (first == std::string::npos) {
1634+ return false ;
1635+ }
1636+ if (first > 0 ) {
1637+ raw = raw.substr (first);
1638+ }
15251639 if (raw.empty ()) {
15261640 return false ;
15271641 }
@@ -1563,7 +1677,20 @@ bool Plugin::DispatchPlayerCommand(edict_t *entity, const std::string &command)
15631677 Say (entity, " [XMP] Can only set team name before the match starts.\n " );
15641678 return true ;
15651679 }
1566- const std::string name = (normalized.size () > 9 ) ? normalized.substr (9 ) : " " ;
1680+ // Extract name after "teamname " prefix, skipping any extra whitespace
1681+ const std::string prefix = " teamname " ;
1682+ std::string name;
1683+ if (normalized.size () > prefix.size ()) {
1684+ name = normalized.substr (prefix.size ());
1685+ // Strip leading/trailing whitespace from the name itself
1686+ const auto first = name.find_first_not_of (" \t\r\n\" " );
1687+ const auto last = name.find_last_not_of (" \t\r\n\" " );
1688+ if (first != std::string::npos) {
1689+ name = name.substr (first, last - first + 1 );
1690+ } else {
1691+ name.clear ();
1692+ }
1693+ }
15671694 if (name.empty () || name.length () > 32 ) {
15681695 Say (entity, " [XMP] Usage: .teamname <name> (max 32 characters).\n " );
15691696 return true ;
@@ -1940,10 +2067,23 @@ void Plugin::OnRoundEnd(int winStatus)
19402067void Plugin::OnRoundRestart ()
19412068{
19422069 Log (" OnRoundRestart" );
2070+ if (pauseRequested_) {
2071+ ApplyPause ();
2072+ }
2073+ if (paused_) {
2074+ // Re-apply pause state after round restart resets it
2075+ if (GetGameRules ()) {
2076+ GetGameRules ()->m_bFreezePeriod = TRUE ;
2077+ }
2078+ }
19432079}
19442080
19452081void Plugin::OnRoundFreezeEnd ()
19462082{
2083+ // If still paused, re-freeze to maintain the pause
2084+ if (paused_ && GetGameRules ()) {
2085+ GetGameRules ()->m_bFreezePeriod = TRUE ;
2086+ }
19472087}
19482088
19492089bool Plugin::OnPlayerSpawnEquip (CBasePlayer *player, bool addDefault, bool equipGame)
0 commit comments