Modified source engine (2017) developed by valve and leaked in 2020. Not for commercial purporses
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

3848 lines
141 KiB

//========= Copyright Valve Corporation, All rights reserved. ============//
#include "cbase.h"
#include "tf_gc_server.h"
#include "gcsdk/gcsdk_auto.h"
#include "tf_gcmessages.h"
#include "tf_player.h"
#include "rtime.h"
// XXX(JohnS): Eventually, we want to send a smaller lobby object to clients. For now, they use the CTFGSLobby, which is
// in shared code for that reason.
#include "tf_lobby_server.h"
#include "tf_gamerules.h"
#include "eiface.h"
#include "cdll_int.h"
#include "econ_item_inventory.h"
#include "gameinterface.h"
#include "client.h"
#include "tier1/convar.h"
#include "tf_matchmaking_shared.h"
#include "tf_quickplay_shared.h"
#include "tf_mann_vs_machine_stats.h"
#include "tf_objective_resource.h"
#include "tf_player.h"
#include "tf_voteissues.h"
#include "player_vs_environment/tf_population_manager.h"
#include "quest_objective_manager.h"
#include "player_resource.h"
#include "tf_player_resource.h"
#include "tf_gamestats.h"
#include "tf_player.h"
#include "tf_match_description.h"
#include "util.h"
#include "tier1/utlqueue.h"
#include "tf_player_resource.h"
#include "tf_gc_shared.h"
#include "tf_party.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
using namespace GCSDK;
// How many minutes before we assume something is FUBAR and reboot if we're empty and waiting for the GC to acknowledge us.
// With valid match data: wait a while. GC could be having trouble, or connectivity issues, and we want to hold on to
// the results for it to come back up. After three hours, assume its us.
const int k_InvalidState_Timeout_With_Match = 60 * 2;
const int k_InvalidState_Timeout_Without_Match = 5;
#ifdef ENABLE_GC_MATCHMAKING
/***********************************************************************************************************************
////////////////////////////////////////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
XXX(JohnS) NOTE The current state of the matchmaking flow through this class is a bit of a mess. Have been
incrementally cleaning things up, but be careful.
UpdateConnectedPlayersAndServerInfo()
This is the heug god function that sync's our state with the GC's state via the Lobby shared object:
- Our actual connected players
- m_pMatchInfo (via GetMatch()) - this represents our match in progress, and should generally mirror the GC, but
*MIGHT NOT*. For instance, when the GC is unavailable this object is locked, and when the GC returns we may be
desync'd. This function is in charge of managing that. Outside code should simply look at the MatchInfo object
and trust that it is the state of the match.
- m_vecReservationExpiryTime - this should be merged into MatchInfo eventually, but is an array of active
reservations and when they expire. This isn't in MatchInfo because in some modes we operate with reservations
but without running a proper Match. When we're running a match, anyone in this vector should be in the MatchInfo
- CTFGSLobby - This is the shared object from the server that represents the match we are hosting. However, it is
*NOT* the article of record on the match. This is due to matches being designed to be resilient to GC connection
loss. Essentially, only this function should be looking at CTFGSLobby and negotiating the state of the actual
match it believes itself to have in MatchInfo.
== Gameserver / GC Authority
- GC forms matches, adds players to matches, passes them to servers
- Servers run matches to completion, have authority on abandons/etc. regardless of GC state
- Servers pass result, including any abandons, to GC. Message is queued if GC is unavailable.
- GC takes match results and does ELO calculation and any stats/etc.
- GC can request players be kicked from matches or matches be canceled
- If more players are needed
- Gameserver requests GC attention with appropriate flag (6v6: Stalled, waiting on complete match, 12v12:
Non-full match)
- GC adds players to lobby, making them part of the match
- If server state is poor (hypothetically: lag, too many abandons, abnormal something or other)
- Game server sends KickLobby to terminate match, sends failed match result
- If GC is unavailable
- Game server still carries out duties, may decide to make changes like end match instead of request late joins
if it decides GC wont be able to provide them.
== Match Start
- GC creates a lobby and hands it to us. UpdateConnectedPlayers tick initializes a MatchInfo struct as
appropriate, accepts players.
== Adding Players
- The GC adds players to the lobby (so, when GC down, matches cannot gain players)
- UpdateConnectedPlayersAndServerInfo ensures that makes sense (it should, though, we no longer have legacy match
types where the GC adds players we shouldn't accept)
- UpdateConnectedPlayers calls AcceptGCReservation, player is added to match and put in reservation list
== Dropping Players
- Case 1: Player is not present, but is in the lobby (GC *might* be down, doesn't matter)
- Player marked missing in MatchInfo by UpdateConnectedPlayers tick
- After a grace period, player marked dropped, as an abandoner in MatchInfo
- PlayerLeftMatch message is sent to tell the GC about their leaving.
- Case 2: Player is dropped from GC lobby
- UpdateConnectedPlayers assumes GC kicked them, marks them dropped from match and kicks them.
- TODO: Ideally there'd be a KickThisGuy GC message, and we'd respond with PlayerLeftMatch, rather than the GC
unilaterally dropping people like this.
- Case 3: Votekicked
- PlayerLeftMatch is sent, from server
- All cases:
- A reliable GC message player-abandoned (or was kicked or never joined) message queued to reconcile this with
the lobby state, but if GC is unavailable it will be informed when it returns.
- Player is marked dropped in MatchInfo
== Team Assignments
- The GC delivers an initial team assignment for each player added to the match. This team assignment does not
change when game teams change sides, see TFGameRules::GameTeamToLobbyTeam and its inverse to map these to game
logic teams (vs TF_GC_TEAM objects)
- All other team changes have to be initiated by a game server message, in modes that allow it, to prevent
race-conditions.
- The NewMatchForLobby message expects the GC to shuffle our teams. We prevent races by not issuing other team
change messages while this message is pending. If we time out waiting for the GC, some modes may start a
speculative server-created match (expecting the GC to come back and respond to that message positively). In
this case, we queue a ChangeMatchPlayerTeams message to stomp any assignments back to our known state,
allowing us to ignore the temporary de-sync (queued messages always get processed in sequence)
- The ChangeMatchPlayerTeams message allows the gameserver to change match player teams mid-game in match modes
that allow it. The game server is in charge of not queuing this message in parallel with NewMatchForLobby
above, or handling the potential race.
- When processing either of these messages, the GC cancels any players that are awaiting acceptance by the
game-server, and re-tries if necessary. This prevents team changes from racing with player-joins which may
have been predicated on differing team layouts.
- The game server does not accept pending players or send any heartbeats until any queued messages have been
responded to. See Queued Messages below.
== Match End
- Match result message provides canonical record of match, is queued to send to GC when available.
- GameServerKickingLobby message dissolves live match if GC is available/tracking it. Queued similarly.
- ** This can happen before or after the match result.
- In MvM, we send potentially multiple victory messages per match -- they can cycle missions and keep winning.
- As of right now, in competitive, we end the match coincident with sending a match result.
- Match ended doesn't necessarily kick players, so a dead/finished match will stick around on our end until
everyone Disconnects, (or the game logic kicks them, e.g. MatchInfo->BEnded + a timeout)
- Ended matches have queued a message to dissolve their lobby, though, so further GC interaction with the match is
not possible, and players are allowed to leave (since they're now allowed to be put in a new match by the GC)
== Queued Messages And Match State And Race Conditions
- Since queued messages are sent in order until confirmed, the GC will always see (eventually) a coherent
story. For instance:
- PlayerLeftMatch - GC marks this player as leaving match
- KickingLobby - GC marks match as finished, result pending
- MatchResult (minus the two players who left) - GC finishes match accounting, marks match complete, missing
players are already noted as leavers so their absence from the result is expected.
- While messages are queued, we do not run the UpdateConnectedPlayers() think. This prevents having to worry about
a fractal of potential edge cases -- we don't look at updated lobby data or send heartbeats while anything we're
trying to tell the GC hasn't been confirmed. This also means we won't send a heartbeat until all such actions
have been confirmed.
- GC message handlers for queued messages do have to handle possible races -- if the GC sends us players while
we're sending a "Reassign Player Team" message, this behavior means we'll stubbornly wait for a response to
the team message before acknowledging any players, allowing the GC to easily resolve the race (in this case,
by canceling or retrying any attempted add-player-match actions)
== Gameserver Crashes
- If GC is available, it handles it, otherwise, match is lost. Gameservers don't currently try to persist this
state.
== Match empties out
- If the match is still going, it should reach ended as everyone in it gets timed out as an abandon.
- If the GC is around, it will revoke the lobby once we inform it everyone has dropped.
- Once the match is marked ended, and the GC concurs and deletes the lobby, we delete MatchInfo
- If the GC is not around, we hang out on the completed match state until it is. We can't exactly take new
matches in the mean time. (but, see k_InvalidState_Timeout_With_Match)
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\////////////////////////////////////////////////////////////
***********************************************************************************************************************/
static const char g_pszIdleKickString[] = "#TF_Idle_kicked";
//ConVar dota_force_upload_match_stats( "dota_force_upload_match_stats", "0", FCVAR_CHEAT, "If enabled, server will upload match stats even when there aren't human players on each side" );
//extern ConVar dota_force_bot_cycle;
extern CServerGameDLL g_ServerGameDLL;
// How long a player can be missing from a MM match before they are dropped and given an abandon. Set to -1 to disable.
ConVar tf_mm_player_disconnect_time_before_abandon( "tf_mm_player_disconnect_time_before_abandon", "180", FCVAR_DEVELOPMENTONLY );
// How quickly we should forgive a match player's disconnected time after they return. At a ratio of 10, 30 minutes of
// connected time would cancel out 3 minutes of disconnected type. Set to 0 to disable.
ConVar tf_mm_player_disconnect_time_forgive_ratio( "tf_mm_player_disconnect_time_forgive_ratio", "10", FCVAR_DEVELOPMENTONLY );
// Any disconnect, no matter for how long, should count as this many seconds of disconnected time. This is because the
// act of reconnecting can be more disruptive than just the absense -- ready-up timers reset, the game may
// pause/unpause, etc..
//
// Currently at 90 -- two rapid rejoins in a row, even with instant loading, will eat up your DC allowance. Note that
// if you take at least 90s to rejoin/load anyway, this would have no effect.
ConVar tf_mm_player_disconnect_time_minimum_penalty( "tf_mm_player_disconnect_time_minimum_penalty", "90", FCVAR_DEVELOPMENTONLY );
ConVar tf_mm_next_map_result_hold_time( "tf_mm_next_map_result_hold_time", "7" );
ConVar tf_mvm_allow_abandon_after_seconds( "tf_mvm_allow_abandon_after_seconds", "600", FCVAR_DEVELOPMENTONLY );
ConVar tf_mvm_allow_abandon_below_players( "tf_mvm_allow_abandon_below_players", "5", FCVAR_DEVELOPMENTONLY );
ConVar tf_allow_server_hibernation( "tf_allow_server_hibernation", "1", FCVAR_NONE, "Allow the server to hibernate when empty." );
#ifdef STAGING_ONLY
ConVar tf_debug_xp_changes( "tf_debug_xp_changes", "0" );
#endif
//DEFINE_LOGGING_CHANNEL_NO_TAGS( LOG_CONSOLE, "Console" );
static CTFGCServerSystem s_TFGCServerSystem;
CTFGCServerSystem *GTFGCClientSystem() { return &s_TFGCServerSystem; }
//bool g_bServerReceivedGCWelcome = false;
int g_gcServerVersion = 0; // Version from the GC
static bool g_bWarnedAboutMaxplayersInMVM = false;
extern ConVar tf_mm_servermode;
extern ConVar tf_mm_trusted;
extern ConVar tf_mm_strict;
// Some reliable messages don't know the matchID yet when they are queued, but we should have it by time they send. This
// helper takes their current match ID and returns the one they should use, for use in OnPrepare().
//
// Returns the current match's ID if:
// - The msg's match ID is 0, and we now have a match ID
//
// Calls AbortInvalidMatchState if:
// - The msg's match ID is not zero, and different from the current match
// - Or they're both zero and we're in a match group that requires match IDs.
//
// NOTE We always wait for all pending messages before accepting new matches, so the above should hold unless something
// got badly confused. Only matches with bServerCreated start without knowing their match ID, and should know it
// by time any message that needs it gets sent. (a previous message in queue should be requesting it)
static uint64 ReliableMsgCheckUpdateMatchID( uint64 nMsgMatchID )
{
uint64 nCurrentMatchID = GTFGCClientSystem()->GetMatch()->m_nMatchID;
Assert( !nMsgMatchID || nMsgMatchID == nCurrentMatchID );
// If we were queued for a match we didn't know the ID of yet, we can now glom it
if ( nCurrentMatchID && nMsgMatchID == 0 )
{
return nCurrentMatchID;
}
else if ( nCurrentMatchID != nMsgMatchID )
{
// Something is bad
GTFGCClientSystem()->AbortInvalidMatchState();
}
else if ( !nCurrentMatchID && !nMsgMatchID )
{
auto *pMatchDesc = GetMatchGroupDescription( GTFGCClientSystem()->GetMatch()->m_eMatchGroup );
if ( !pMatchDesc || pMatchDesc->BRequiresMatchID() )
{
GTFGCClientSystem()->AbortInvalidMatchState();
}
}
return nMsgMatchID;
}
//-----------------------------------------------------------------------------
// Reliable messages
//-----------------------------------------------------------------------------
class ReliableMsgNewMatchForLobby
: public CJobReliableMessageBase < ReliableMsgNewMatchForLobby,
CMsgGCNewMatchForLobbyRequest, k_EMsgGC_NewMatchForLobbyRequest,
CMsgGCNewMatchForLobbyResponse, k_EMsgGC_NewMatchForLobbyResponse >
{
public:
void OnReply( Reply_t &msgReply )
{ GTFGCClientSystem()->NewMatchForLobbyResponse( msgReply.Body().success() ); }
void OnPrepare()
{ Assert( Msg().Body().current_match_id() == GTFGCClientSystem()->GetMatch()->m_nMatchID ); }
const char *MsgName() { return "NewMatchForLobby"; }
void InitDebugString( CUtlString &dbgStr )
{
dbgStr.Format( "Match %llx, Lobby %llx, Next Map %d",
Msg().Body().current_match_id(),
Msg().Body().lobby_id(),
Msg().Body().next_map_id() );
}
};
//-----------------------------------------------------------------------------
class ReliableMsgChangeMatchPlayerTeams
: public CJobReliableMessageBase < ReliableMsgChangeMatchPlayerTeams,
CMsgGCChangeMatchPlayerTeamsRequest, k_EMsgGC_ChangeMatchPlayerTeamsRequest,
CMsgGCChangeMatchPlayerTeamsResponse, k_EMsgGC_ChangeMatchPlayerTeamsResponse >
{
public:
void OnReply( Reply_t &msgReply )
{ GTFGCClientSystem()->ChangeMatchPlayerTeamsResponse( msgReply.Body().success() ); }
// May have been queued for a pending match
void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); }
const char *MsgName() { return "ChangeMatchPlayerTeams"; }
void InitDebugString( CUtlString &dbgStr )
{
dbgStr.Format( "Match %llx, Lobby %llx, %d members",
Msg().Body().match_id(), Msg().Body().lobby_id(), Msg().Body().member_size() );
}
};
//-----------------------------------------------------------------------------
class ReliableMsgMvMVictory
: public CJobReliableMessageBase < ReliableMsgMvMVictory,
CMsgMvMVictory, k_EMsgGCMvMVictory,
CMsgMvMMannUpVictoryReply, k_EMsgGCMvMVictoryReply >
{
public:
const char *MsgName() { return "MvMVictory"; }
void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Lobby %016llx", Msg().Body().lobby_id() ); }
};
//-----------------------------------------------------------------------------
class ReliableMsgGameServerKickingLobby
: public CJobReliableMessageBase < ReliableMsgGameServerKickingLobby,
CMsgGameServerKickingLobby, k_EMsgGCGameServerKickingLobby,
CMsgGameServerKickingLobbyResponse, k_EMsgGCGameServerKickingLobbyResponse >
{
public:
// May have been queued for a pending match
void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); }
const char *MsgName() { return "GameServerKickingLobby"; }
void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Match %llx, Lobby %llx",
Msg().Body().match_id(), Msg().Body().lobby_id() ); }
};
//-----------------------------------------------------------------------------
class ReliableMsgPlayerLeftMatch
: public CJobReliableMessageBase < ReliableMsgPlayerLeftMatch,
CMsgPlayerLeftMatch, k_EMsgGCPlayerLeftMatch,
CMsgPlayerLeftMatchResponse, k_EMsgGCPlayerLeftMatchResponse >
{
public:
// May have been queued for a pending match
void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); }
const char *MsgName() { return "PlayerLeftMatch"; }
void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Player %s, Match %llx, Lobby %llx",
CSteamID( Msg().Body().steam_id() ).Render(),
Msg().Body().match_id(), Msg().Body().lobby_id() ); }
};
//-----------------------------------------------------------------------------
// Sent for players who where votekicked after leaving the match
// - That is, were being votekicked when they left, it later passed, to resolve the race-condition by posthumously
// upgrading their penalty GC-side)
class ReliableMsgPlayerVoteKickedAfterLeavingMatch
: public CJobReliableMessageBase < ReliableMsgPlayerVoteKickedAfterLeavingMatch,
CMsgPlayerVoteKickedAfterLeavingMatch, k_EMsgGCPlayerVoteKickedAfterLeavingMatch,
CMsgPlayerVoteKickedAfterLeavingMatchResponse, k_EMsgGCPlayerVoteKickedAfterLeavingMatchResponse >
{
public:
// May have been queued for a pending match
void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); }
const char *MsgName() { return "PlayerVoteKickedAfterLeavingMatch"; }
void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Player %s, Match %llx, Lobby %llx",
CSteamID( Msg().Body().steam_id() ).Render(),
Msg().Body().match_id(), Msg().Body().lobby_id() ); }
};
//-----------------------------------------------------------------------------
class ReliableMsgMatchResult
: public CJobReliableMessageBase < ReliableMsgMatchResult,
CMsgGC_Match_Result, k_EMsgGC_Match_Result,
CMsgGC_Match_ResultResponse, k_EMsgGC_Match_ResultResponse >
{
public:
// May have been queued for a pending match
void OnPrepare() { Msg().Body().set_match_id( ReliableMsgCheckUpdateMatchID( Msg().Body().match_id() ) ); }
const char *MsgName() { return "MatchResult"; }
void InitDebugString( CUtlString &dbgStr ) { dbgStr.Format( "Match %016llx", Msg().Body().match_id() ); }
};
//-----------------------------------------------------------------------------
// CMvMVictoryInfo
//-----------------------------------------------------------------------------
void CMvMVictoryInfo::Init ( CTFGSLobby *pLobby )
{
if ( !pLobby )
{
MMLog( "CTFGCServerSystem::MvMVictory() -- no lobby, so not sending results to GC\n" );
return;
}
m_nLobbyId = pLobby->GetGroupID();
m_sChallengeName = pLobby->GetMissionName();
#ifdef USE_MVM_TOUR
if ( IsMannUpGroup( pLobby->GetMatchGroup() ) )
{
const char *pszTourName = pLobby->GetMannUpTourName();
Assert( pszTourName );
m_sMannUpTourOfDuty = pszTourName;
}
#endif // USE_MVM_TOUR
m_tEventTime = CRTime::RTime32TimeCur();
m_vPlayerIds.RemoveAll();
m_vSquadSurplus.RemoveAll();
for ( int iMember = 0; iMember < pLobby->GetNumMembers(); iMember++ )
{
m_vPlayerIds.AddToTail( pLobby->GetMember( iMember ).ConvertToUint64() );
m_vSquadSurplus.AddToTail( pLobby->GetMemberDetails( iMember )->squad_surplus() );
}
}
//-----------------------------------------------------------------------------
// CCompetitiveMatchInfo
//-----------------------------------------------------------------------------
CMatchInfo::CMatchInfo( const CTFGSLobby *pLobby )
: m_nMatchID( pLobby->GetMatchID() )
, m_nLobbyID( pLobby->GetGroupID() )
, m_eMatchGroup( pLobby->GetMatchGroup() )
, m_uLobbyFlags( pLobby->GetFlags() )
, m_uAverageRank( pLobby->Obj().average_rank() )
, m_rtMatchCreated( CRTime::RTime32TimeCur() )
, m_unEventTeamStatus( pLobby->Obj().is_war_match() )
, m_bFirstPersonActive( false )
, m_nBotsAdded( 0 )
, m_bServerCreated( false )
, m_strMapName( pLobby->GetMapName() )
, m_bMatchEnded( false )
, m_bSentResult( false )
, m_nGCMatchSize( pLobby->Obj().has_fixed_match_size() ? pLobby->Obj().fixed_match_size() : 0 )
#ifdef STAGING_ONLY
, m_flBronzePercentile( 0.5f )
, m_flSilverPercentile( 0.65f )
, m_flGoldPercentile( 0.8f )
#else
, m_flBronzePercentile( 0.6f )
, m_flSilverPercentile( 0.75f )
, m_flGoldPercentile( 0.9f )
#endif
{
uint32 nNumCompLevels = GetMatchGroupDescription( k_nMatchGroup_Casual_6v6 )->m_pProgressionDesc->GetNumLevels();
m_vDailyStatsRankData.EnsureCapacity( nNumCompLevels );
RequestGCRankData();
}
CMatchInfo::~CMatchInfo()
{
m_vMatchRankData.PurgeAndDeleteElements();
}
//-----------------------------------------------------------------------------
//
//-----------------------------------------------------------------------------
CMatchInfo::CMatchInfo()
{
// Don't do this
Assert( 0 );
}
//-----------------------------------------------------------------------------
//
//-----------------------------------------------------------------------------
CMatchInfo::CMatchInfo( const CMatchInfo &otherinfo )
{
// Don't do this
Assert( 0 );
}
//-----------------------------------------------------------------------------
//
//-----------------------------------------------------------------------------
CMatchInfo::PlayerMatchData_t::PlayerMatchData_t( const PlayerMatchData_t& rhs )
: m_mapXPAccumulation( DefLessFunc( CMsgTFXPSource::XPSourceType ) )
{
steamID = rhs.steamID;
uPartyID = rhs.uPartyID;
eGCTeam = rhs.eGCTeam;
bDropped = rhs.bDropped;
bConnected = rhs.bConnected;
rtJoinedMatch = CRTime::RTime32TimeCur();
nVoteKickAttempts = rhs.nVoteKickAttempts;
nDisconnectedSeconds = 0;
nScoreMedal = rhs.nScoreMedal;
nKillsMedal = rhs.nKillsMedal;
nDamageMedal = rhs.nDamageMedal;
nHealingMedal = rhs.nHealingMedal;
nSupportMedal = rhs.nSupportMedal;
bLateJoin = rhs.bLateJoin;
nScore = rhs.nScore;
rtLastActiveEvent = CRTime::RTime32TimeCur();
bAlwaysSafeToLeave = rhs.bAlwaysSafeToLeave;
bEverConnected = rhs.bEverConnected;
bDropWasAbandon = rhs.bDropWasAbandon;
eDropReason = rhs.eDropReason;
nConnectingButNotActiveIndex = rhs.nConnectingButNotActiveIndex;
bPlayed = false;
unMMSkillRating = rhs.unMMSkillRating;
nDrilloRatingDelta = 0;
unClassesPlayed = 0u;
}
//-----------------------------------------------------------------------------
//
//-----------------------------------------------------------------------------
MM_PlayerConnectionState_t CMatchInfo::PlayerMatchData_t::GetConnectionState() const
{
if ( bConnected )
{
return nConnectingButNotActiveIndex == 0 ? MM_CONNECTED : MM_LOADING;
}
else
{
return bEverConnected ? MM_DISCONNECTED : MM_CONNECTING;
}
}
//-----------------------------------------------------------------------------
//
//-----------------------------------------------------------------------------
void CMatchInfo::PlayerMatchData_t::UpdateClassesPlayed( int nClass )
{
Assert( nClass >= TF_FIRST_NORMAL_CLASS && nClass <= TF_LAST_NORMAL_CLASS );
unClassesPlayed = unClassesPlayed | ( 1 << nClass );
}
//-----------------------------------------------------------------------------
//
//-----------------------------------------------------------------------------
void CMatchInfo::PlayerMatchData_t::OnConnected( int nEntindex )
{
if ( bConnected )
{
// This is before steamID validation, so make sure we don't add a path that would reward spoof connections.
Assert( !"Player connecting is marked connected" );
return;
}
nConnectingButNotActiveIndex = nEntindex;
// Mark connected.
bConnected = true;
bEverConnected = true;
RTime32 now = CRTime::RTime32TimeCur();
MMLog( "Match player %s reconnected into slot %d, last active %u seconds ago.\n",
steamID.Render(), nConnectingButNotActiveIndex, now - rtLastActiveEvent );
}
//-----------------------------------------------------------------------------
//
//-----------------------------------------------------------------------------
void CMatchInfo::PlayerMatchData_t::OnActive()
{
nConnectingButNotActiveIndex = 0;
CMatchInfo* pMatch = GTFGCClientSystem()->GetMatch();
Assert( pMatch);
if ( pMatch && !pMatch->m_bFirstPersonActive )
{
MMLog( "Match going active\n" );
pMatch->m_bFirstPersonActive = true;
}
// Disconnected seconds for the time since they were last active, including DC'd time and time spent loading. This
// prevents people who crash but rejoin quickly being able to be not-in-game for far longer than intended. Since we
// already marked them connected, the abandon think won't touch them if this accumulation goes over the limit, but
// it will count against them if they drop again.
RTime32 now = CRTime::RTime32TimeCur();
RTime32 missing = now - rtLastActiveEvent;
// See this convar's comment for why we do this.
RTime32 minimum = (RTime32)Clamp( tf_mm_player_disconnect_time_minimum_penalty.GetInt(), 0, INT_MAX );
nDisconnectedSeconds += Max( missing, minimum );
rtLastActiveEvent = now;
}
//-----------------------------------------------------------------------------
// Add a rank bucket stats vector
//-----------------------------------------------------------------------------
void CMatchInfo::SetDailyRankData( DailyStatsRankBucket_t vecRankData )
{
m_vDailyStatsRankData.AddToTail( vecRankData );
}
//-----------------------------------------------------------------------------
// Request the competitive daily stats rollup from the GC
//-----------------------------------------------------------------------------
bool CMatchInfo::RequestGCRankData( void )
{
if ( !GetMatchGroupDescription( m_eMatchGroup ) ||
!GetMatchGroupDescription( m_eMatchGroup )->m_params.m_bDistributePerformanceMedals )
{
return false;
}
GCSDK::CProtoBufMsg< CMsgGC_DailyCompetitiveStatsRollup > msg( k_EMsgGC_DailyCompetitiveStatsRollup );
return GCClientSystem()->BSendMessage( msg );
}
//-----------------------------------------------------------------------------
void CMatchInfo::AddPlayer( const PlayerMatchData_t &player, int nEntIndex, bool bActive )
{
PlayerMatchData_t* pOldPlayerMatchData = GetMatchDataForPlayer( player.steamID );
if ( pOldPlayerMatchData )
{
// Already have data?
if ( pOldPlayerMatchData->bDropped )
{
// Returning a player that had dropped from the match. Re-create their entry as a fresh player, so the
// constructor re-does everything.
MMLog( "Player %s re-added to match they previously dropped from, replacing existing entry\n",
player.steamID.Render() );
m_vMatchRankData.FindAndRemove( pOldPlayerMatchData );
delete pOldPlayerMatchData;
pOldPlayerMatchData = nullptr;
}
else
{
// This player is already in the match
Assert( false );
MMLog( "!! Player %s being added to the match, but they are already present\n",
player.steamID.Render() );
return;
}
}
PlayerMatchData_t* pPlayerMatchData = new PlayerMatchData_t( player );
m_vMatchRankData.AddToTail( pPlayerMatchData );
if ( nEntIndex != 0 )
{
pPlayerMatchData->OnConnected( nEntIndex );
}
if ( bActive )
{
pPlayerMatchData->OnActive();
}
}
//-----------------------------------------------------------------------------
void CMatchInfo::AddPlayer( CSteamID steamID, const CTFLobbyMember *pMemberData, bool bIsLateJoin, int nEntIndex, bool bActive )
{
PlayerMatchData_t playerMatchData( steamID, pMemberData );
playerMatchData.unMMSkillRating = pMemberData->skillrating();
playerMatchData.bLateJoin = bIsLateJoin;
AddPlayer( playerMatchData, nEntIndex, bActive );
}
//-----------------------------------------------------------------------------
void CMatchInfo::DropPlayer( CSteamID steamID, TFMatchLeaveReason eReason, bool bWasAbandon )
{
CMatchInfo::PlayerMatchData_t *pPlayerMatchData = GetMatchDataForPlayer( steamID );
AssertMsg( pPlayerMatchData, "If we have competitive match info, this player should be known" );
if ( pPlayerMatchData )
{
if ( pPlayerMatchData->bDropped )
{
MMLog( "!! Double-dropping player %s\n", steamID.Render() );
Assert( false );
}
pPlayerMatchData->bDropped = true;
pPlayerMatchData->eDropReason = eReason;
pPlayerMatchData->bDropWasAbandon = bWasAbandon;
}
}
//-----------------------------------------------------------------------------
const CMatchInfo::PlayerMatchData_t* CMatchInfo::GetMatchDataForPlayer( CSteamID steamID ) const
{
return const_cast<CMatchInfo*>(this)->GetMatchDataForPlayer( steamID );
}
//-----------------------------------------------------------------------------
CMatchInfo::PlayerMatchData_t* CMatchInfo::GetMatchDataForPlayer( CSteamID steamID )
{
FOR_EACH_VEC( m_vMatchRankData, i )
{
if ( m_vMatchRankData[i]->steamID == steamID )
return ( m_vMatchRankData[i] );
}
return NULL;
}
//-----------------------------------------------------------------------------
CMatchInfo::PlayerMatchData_t* CMatchInfo::GetMatchDataForPlayer( int idx )
{
return m_vMatchRankData[idx];
}
//-----------------------------------------------------------------------------
int CMatchInfo::GetNumTotalMatchPlayers() const
{
return m_vMatchRankData.Count();
}
//-----------------------------------------------------------------------------
int CMatchInfo::GetNumActiveMatchPlayers() const
{
int nActivePlayers = 0;
FOR_EACH_VEC( m_vMatchRankData, idx )
{
nActivePlayers += !m_vMatchRankData[idx]->bDropped;
}
return nActivePlayers;
}
//-----------------------------------------------------------------------------
int CMatchInfo::GetNumActiveMatchPlayersForTeam( int nTeam ) const
{
int nActivePlayers = 0;
FOR_EACH_VEC( m_vMatchRankData, idx )
{
if ( !m_vMatchRankData[idx]->bDropped )
{
if ( m_vMatchRankData[idx]->eGCTeam == nTeam )
{
nActivePlayers++;
}
}
}
return nActivePlayers;
}
//-----------------------------------------------------------------------------
int CMatchInfo::GetTotalSkillRatingForTeam( int nTeam ) const
{
// Re-evaluate this when skillrating might be for other backends
FixmeMMRatingBackendSwapping();
int nSkillRating = 0;
FOR_EACH_VEC( m_vMatchRankData, idx )
{
if ( !m_vMatchRankData[idx]->bDropped )
{
if ( m_vMatchRankData[idx]->eGCTeam == nTeam )
{
nSkillRating += m_vMatchRankData[idx]->unMMSkillRating;
}
}
}
return nSkillRating;
}
//-----------------------------------------------------------------------------
int CMatchInfo::GetNumConnectedMatchPlayers() const
{
int nConnectedPlayers = 0;
FOR_EACH_VEC( m_vMatchRankData, idx )
{
nConnectedPlayers += ( m_vMatchRankData[idx]->bConnected && !m_vMatchRankData[idx]->bDropped );
}
return nConnectedPlayers;
}
//-----------------------------------------------------------------------------
uint32 CMatchInfo::GetCanonicalMatchSize() const
{
return m_nGCMatchSize ? m_nGCMatchSize : GetMatchGroupDescription( m_eMatchGroup )->GetMatchSize();
}
//-----------------------------------------------------------------------------
void CMatchInfo::GiveXPRewardToPlayerForAction( CSteamID steamID, CMsgTFXPSource::XPSourceType eType, int nCount )
{
// Needs to be a positive number!
if ( nCount <= 0 )
return;
GiveXPDirectly( steamID, eType, ceil( (float)nCount * g_XPSourceDefs[ eType ].m_flValueMultiplier ), true );
}
//-----------------------------------------------------------------------------
void CMatchInfo::GiveXPDirectly( CSteamID steamID, CMsgTFXPSource::XPSourceType eType, int nAmount, bool bCanAwardBonusXP )
{
const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( m_eMatchGroup );
if ( !pMatchDesc || !pMatchDesc->BUsesXP() || nAmount <= 0 )
{
return;
}
PlayerMatchData_t *pMatchPlayer = GetMatchDataForPlayer( steamID );
if ( pMatchPlayer && !pMatchPlayer->bDropped )
{
CMsgTFXPSource* pSource = NULL;
auto idx = pMatchPlayer->m_mapXPAccumulation.Find( eType );
if ( idx == pMatchPlayer->m_mapXPAccumulation.InvalidIndex() )
{
idx = pMatchPlayer->m_mapXPAccumulation.Insert( eType, 0.f );
}
// You can only draw from the bonus pool if you GAINED xp
if ( nAmount > 0 && bCanAwardBonusXP )
{
FOR_EACH_VEC_BACK( pMatchPlayer->m_vecXPBonusPools, i )
{
PlayerMatchData_t::XPBonusPool_t& xpMultiplier = pMatchPlayer->m_vecXPBonusPools[ i ];
// We do this so when specifying the multiplier, you can say you want the multiplier to be
int nBonusAmount = ceil( nAmount * xpMultiplier.m_flMultiplier );
// If there's a maximum amount to give for this bonus, subtract from the total
// and remove this bonus if the pool is emptied
Assert( xpMultiplier.m_nBonusPoolRemaining > 0 );
nBonusAmount = Min( nBonusAmount, xpMultiplier.m_nBonusPoolRemaining );
xpMultiplier.m_nBonusPoolRemaining -= nBonusAmount;
// Save the type so we can recursively pass it below
CMsgTFXPSource::XPSourceType eBonusType = xpMultiplier.m_eType;
// If there's no more in the pool, then we can remove this from the list
if ( xpMultiplier.m_nBonusPoolRemaining <= 0 )
{
// We're going backwards, so this is ok
pMatchPlayer->m_vecXPBonusPools.Remove( i );
}
// Give the bonus
GiveXPDirectly( steamID, eBonusType, nBonusAmount, false );
}
}
// Accumulate in the map
pMatchPlayer->m_mapXPAccumulation[ idx ] += nAmount;
int nAccum = pMatchPlayer->m_mapXPAccumulation[ idx ];
// Don't make a XPSource proto object if there's nothing to even report
if ( nAccum == 0 )
return;
// Find the type if it exists.
for( int i=0; i < pMatchPlayer->m_XPBreakdown.sources_size(); ++i )
{
if ( pMatchPlayer->m_XPBreakdown.sources( i ).type() == eType )
{
pSource = pMatchPlayer->m_XPBreakdown.mutable_sources( i );
break;
}
}
// Create a new one if we need to
if ( pSource == NULL )
{
pSource = pMatchPlayer->m_XPBreakdown.add_sources();
pSource->set_account_id( steamID.GetAccountID() );
pSource->set_match_group( m_eMatchGroup );
pSource->set_type( eType );
pSource->set_match_id( m_nMatchID );
pSource->set_amount( 0 );
}
#ifdef STAGING_ONLY
if ( tf_debug_xp_changes.GetBool() && nAccum != pSource->amount() )
{
CBasePlayer* pPlayer = UTIL_PlayerBySteamID( steamID );
if ( pPlayer )
{
Msg( "%s received %d %s xp\n", pPlayer->GetPlayerName(),
nAccum - pSource->amount(),
CMsgTFXPSource_XPSourceType_descriptor()->value( eType )->name().c_str() );
}
}
#endif
// Update the amount
pSource->set_amount( nAccum );
}
}
//-----------------------------------------------------------------------------
void CMatchInfo::GiveXPBonus( CSteamID steamID,
CMsgTFXPSource_XPSourceType eType,
float flMultipler,
int nBonusPool )
{
const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( m_eMatchGroup );
if ( !pMatchDesc || !pMatchDesc->BUsesXP() )
{
return;
}
PlayerMatchData_t *pMatchPlayer = GetMatchDataForPlayer( steamID );
if ( pMatchPlayer && !pMatchPlayer->bDropped )
{
// Find existing entry if there is one
auto idx = pMatchPlayer->m_vecXPBonusPools.InvalidIndex();
FOR_EACH_VEC( pMatchPlayer->m_vecXPBonusPools, i )
{
// Found it
if( pMatchPlayer->m_vecXPBonusPools[ i ].m_eType == eType )
{
idx = i;
break;
}
}
// Create new entry if we didnt have an existing one
if ( idx == pMatchPlayer->m_vecXPBonusPools.InvalidIndex() )
{
idx = pMatchPlayer->m_vecXPBonusPools.AddToTail();
}
// Add bonus
PlayerMatchData_t::XPBonusPool_t& currentXPMultiplier = pMatchPlayer->m_vecXPBonusPools[ idx ];
currentXPMultiplier.m_nBonusPoolRemaining += nBonusPool;
currentXPMultiplier.m_eType = eType;
currentXPMultiplier.m_flMultiplier = Max( currentXPMultiplier.m_flMultiplier, flMultipler );
}
}
#ifdef STAGING_ONLY
CON_COMMAND( give_xp_bonus, "Gives the player with the specified name an xp boost. Usage: give_xp_bonus <name> <type> <multiplier> <bonus_pool>" )
{
if ( args.ArgC() != 5 )
{
Msg( "Incorrect arguments. Usage: give_xp_bonus <name> <type> <multiplier> <bonus_pool>\n" );
return;
}
CBasePlayer* pPlayer = UTIL_PlayerByName( args.Arg( 1 ) );
if ( !pPlayer )
{
Msg( "No player named %s\n", args.Arg( 1 ) );
return;
}
if ( !GTFGCClientSystem()->GetMatch() )
{
Msg( "Not running a match\n" );
return;
}
CMsgTFXPSource_XPSourceType nType = (CMsgTFXPSource_XPSourceType)atoi( args.Arg( 2 ) );
if ( nType < CMsgTFXPSource_XPSourceType_XPSourceType_MIN
|| nType >= CMsgTFXPSource_XPSourceType_NUM_SOURCE_TYPES )
{
Msg( "Type is not a valid type!\n" );
return;
}
CSteamID steamID;
pPlayer->GetSteamID( &steamID );
GTFGCClientSystem()->GetMatch()->GiveXPBonus( steamID,
nType,
atof( args.Arg( 3 ) ),
atoi( args.Arg( 4 ) ) );
}
#endif
//-----------------------------------------------------------------------------
bool CMatchInfo::BPlayerSafeToLeaveMatch( CSteamID steamID )
{
PlayerMatchData_t *pMatchPlayer = this->GetMatchDataForPlayer( steamID );
// Right now, you cannot leave while the match is running
bool bSafe = m_bMatchEnded || !pMatchPlayer || pMatchPlayer->bDropped || pMatchPlayer->bAlwaysSafeToLeave;
// The match description might have special exceptions
if ( !bSafe && pMatchPlayer )
{
bSafe = bSafe || GetMatchGroupDescription( m_eMatchGroup )->BMatchIsSafeToLeaveForPlayer( this, pMatchPlayer );
}
return bSafe;
}
//-----------------------------------------------------------------------------
// Determine the performance ranking of each player after a competitive match
//-----------------------------------------------------------------------------
bool CMatchInfo::CalculatePlayerMatchRankData( void )
{
Assert( TFGameRules() );
if ( !TFGameRules() )
return false;
CTFPlayerResource *pTFResource = dynamic_cast< CTFPlayerResource* >( g_pPlayerResource );
if ( !pTFResource )
return false;
CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
if ( !pMatch )
return false;
if ( !m_vDailyStatsRankData.Count() )
{
Warning( "CalculatePlayerMatchRankData(): DailyStatsRankData is empty\n" );
return false;
}
const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( pMatch->m_eMatchGroup );
if ( !pMatchDesc ||
!pMatchDesc->m_pProgressionDesc ||
!pMatchDesc->m_params.m_bDistributePerformanceMedals )
{
return false;
}
CUtlVector < CTFPlayer* > vecPlayers;
CollectHumanPlayers( &vecPlayers );
FOR_EACH_VEC( vecPlayers, i )
{
if ( !vecPlayers[i] )
continue;
CSteamID steamID;
if ( !vecPlayers[i]->GetSteamID( &steamID ) )
continue;
PlayerStats_t *pStats = CTF_GameStats.FindPlayerStats( vecPlayers[i] );
CMatchInfo::PlayerMatchData_t *matchData = GetMatchDataForPlayer( steamID );
if ( !matchData || !pStats )
{
Warning( "Missing player data in CalculatePlayerMatchRankData\n" );
Assert( false );
continue;
}
// Get player's competitive rank
FixmeMMRatingBackendSwapping(); // This is assuming we're using primary skill rating for rank
uint32 unRank = pMatchDesc->m_pProgressionDesc->GetLevelForExperience( matchData->unMMSkillRating ).m_nLevelNum;
int nRankIndex = -1;
// Let's find the typical stats for your rank
FOR_EACH_VEC( m_vDailyStatsRankData, j )
{
if ( unRank == m_vDailyStatsRankData[j].nRank )
{
#ifndef STAGING_ONLY
if ( m_vDailyStatsRankData[j].nRecords < 10 )
{
Warning( "CalculatePlayerMatchRankData(): Too few stat entries (%d) for rank %d\n", m_vDailyStatsRankData[j].nRecords, unRank );
return false;
}
#endif // !STAGING_ONLY
nRankIndex = j;
break;
}
}
uint32 unScoreMedal = GetRankForStat( RankStat_Score, nRankIndex, pTFResource->GetTotalScore( vecPlayers[i]->entindex() ) );
uint32 unKillsMedal = GetRankForStat( RankStat_Kills, nRankIndex, pStats->statsAccumulated.m_iStat[TFSTAT_KILLS] );
uint32 unDamageMedal = GetRankForStat( RankStat_Damage, nRankIndex, pStats->statsAccumulated.m_iStat[TFSTAT_DAMAGE] );
uint32 unHealingMedal = GetRankForStat( RankStat_Healing, nRankIndex, pStats->statsAccumulated.m_iStat[TFSTAT_HEALING] );
uint32 unSupportMedal = GetRankForStat( RankStat_Support, nRankIndex, TFGameRules()->CalcPlayerSupportScore( &pStats->statsAccumulated, vecPlayers[i]->entindex() ) );
matchData->nScoreMedal = unScoreMedal;
matchData->nKillsMedal = unKillsMedal;
matchData->nDamageMedal = unDamageMedal;
matchData->nHealingMedal = unHealingMedal;
matchData->nSupportMedal = unSupportMedal;
}
return true;
}
//-----------------------------------------------------------------------------
//
//-----------------------------------------------------------------------------
bool CMatchInfo::CalculateMatchSkillRatingAdjustments( int iWinningTeam )
{
// This is assuming skill rating is drillo,and doing a client-side prediction on it
FixmeMMRatingBackendSwapping();
if ( !iWinningTeam )
{
Log( "CalculateMatchSkillRatingAdjustments(): Invalid team!\n" );
return false;
}
EMatchGroup matchGroup = m_eMatchGroup;
if ( !IsLadderGroup( matchGroup ) )
{
Assert( false );
Log( "CalculateMatchSkillRatingAdjustments(): Match %llu has an invalid MatchGroup (%i)\n", m_nMatchID, (int)matchGroup );
return false;
}
const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( matchGroup );
if ( !pMatchDesc || !pMatchDesc->m_pProgressionDesc )
{
Log( "CalculateMatchSkillRatingAdjustments(): Match has bogus MatchGroupDescription\n" );
return false;
}
CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch();
if ( !pMatch )
{
Log( "CalculateMatchSkillRatingAdjustments(): Match has bogus CMatchInfo\n" );
return false;
}
int nWinnerTotal = 0;
int nLoserTotal = 0;
uint32 unWinningPlayers = 0u;
uint32 unLosingPlayers = 0u;
// Gather data so we can figure out rating adjustments
for ( int i = 0; i < GetNumTotalMatchPlayers(); i++ )
{
CMatchInfo::PlayerMatchData_t *pPlayerInfo = GetMatchDataForPlayer( i );
Assert( pPlayerInfo );
if ( !pPlayerInfo || pPlayerInfo->bDropped )
continue;
if ( TFGameRules()->GetGameTeamForGCTeam( pPlayerInfo->eGCTeam ) == iWinningTeam )
{
nWinnerTotal += pPlayerInfo->unMMSkillRating;
++unWinningPlayers;
}
else
{
nLoserTotal += pPlayerInfo->unMMSkillRating;
++unLosingPlayers;
}
}
if ( pMatchDesc->m_params.m_bRequireCompleteMatch && ( unWinningPlayers + unLosingPlayers != GetCanonicalMatchSize() ) )
{
Assert( false );
Log( "CalculateMatchSkillRatingAdjustments(): Match %llu has invalid team size(s): %d vs %d\n",
m_nMatchID, unWinningPlayers, unLosingPlayers );
}
int nTeamSize = ( pMatch->GetCanonicalMatchSize() % 2 ) ? ( pMatch->GetCanonicalMatchSize() / 2 + 1 ) : ( pMatch->GetCanonicalMatchSize() / 2 );
int nWinningTeamAverage = (float)nWinnerTotal / Max( nTeamSize, 1 );
int nLosingTeamAverage = (float)nLoserTotal / Max( nTeamSize, 1 );
int nRatingDiff = nLosingTeamAverage - nWinningTeamAverage;
// Determine adjustment based on difference between teams
const int nChange = RemapValClamped( nRatingDiff, /* from */ -(float)k_unDrilloRating_MaxDifference, (float)k_unDrilloRating_MaxDifference,
/* to */ (float)k_nDrilloRating_MinRatingAdjust, (float)k_nDrilloRating_Ladder_MaxRatingAdjust );
// Cap loss for low-rated teams, but not low-rated winners. This breaks the loose "sort-of-zero-sum" system we have, but that's ok in the lower range.
const int nLoserChange = ( nLosingTeamAverage <= k_unDrilloRating_Ladder_LowSkill ) ? Min( nChange, k_nDrilloRating_Ladder_MaxLossAdjust_LowRank ) : nChange;
// Rating delta update
for ( int i = 0; i < GetNumTotalMatchPlayers(); i++ )
{
CMatchInfo::PlayerMatchData_t *pPlayerInfo = GetMatchDataForPlayer( i );
Assert( pPlayerInfo );
if ( !pPlayerInfo )
continue;
int nAmount = nChange;
if ( pPlayerInfo->BDropWasAbandon() )
{
// Abandon
nAmount = -k_nDrilloRating_Ladder_MaxRatingAdjust;
if ( m_eMatchGroup == k_nMatchGroup_Ladder_6v6 )
{
GiveXPDirectly( pPlayerInfo->steamID, CMsgTFXPSource_XPSourceType::CMsgTFXPSource_XPSourceType_SOURCE_COMPETITIVE_ABANDON, nAmount );
}
}
else if ( TFGameRules()->GetGameTeamForGCTeam( pPlayerInfo->eGCTeam ) != iWinningTeam )
{
// Loss
nAmount = -nLoserChange;
}
pPlayerInfo->nDrilloRatingDelta = nAmount;
// Scoreboard
IGameEvent *pEvent = gameeventmanager->CreateEvent( "competitive_stats_update" );
if ( pEvent )
{
CBasePlayer *pPlayer = UTIL_PlayerBySteamID( pPlayerInfo->steamID );
if ( !pPlayer )
continue;
pEvent->SetInt( "index", pPlayer->entindex() );
pEvent->SetInt( "rating", pPlayerInfo->unMMSkillRating );
// This is the only place this guy is used. We should eventually have the GC send down results and use that
// instead of running this prediction step here.
pEvent->SetInt( "delta", pPlayerInfo->nDrilloRatingDelta );
CMatchInfo::PlayerMatchData_t *pMatchRankData = GetMatchDataForPlayer( pPlayerInfo->steamID );
pEvent->SetInt( "score_rank", pMatchRankData ? pMatchRankData->nScoreMedal : 0 ); // medal won (if any)
pEvent->SetInt( "kills_rank", pMatchRankData ? pMatchRankData->nKillsMedal : 0 ); //
pEvent->SetInt( "damage_rank", pMatchRankData ? pMatchRankData->nDamageMedal : 0 ); //
pEvent->SetInt( "healing_rank", pMatchRankData ? pMatchRankData->nHealingMedal : 0 ); //
pEvent->SetInt( "support_rank", pMatchRankData ? pMatchRankData->nSupportMedal : 0 ); //
gameeventmanager->FireEvent( pEvent );
}
}
return true;
}
//-----------------------------------------------------------------------------
// Returns the medal rank (if any) for this stat
//-----------------------------------------------------------------------------
int CMatchInfo::GetRankForStat( RankStatType_t statType, int nRankIndex, uint32 nValue )
{
if ( !m_vDailyStatsRankData.IsValidIndex( nRankIndex ) )
return StatMedal_None;
// Get match duration, so we can scale values accordingly (total time won't have last round time included yet)
uint16 nMatchDuration = CTF_GameStats.m_currentMap.m_Header.m_iTotalTime + ( gpGlobals->curtime - TFGameRules()->GetRoundStart() );
// Assume 9 minute average match duration; TO DO: Use actual values generated from matchresults table
uint16 nAverageMatchDuration = 9 * 60;
// Adjusted Value
float flStatAdjustment = ( float ) nAverageMatchDuration / ( float ) nMatchDuration;
flStatAdjustment = clamp( flStatAdjustment, 0.33f, 3.0f );
nValue = nValue * flStatAdjustment;
uint32 unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgScore;
uint32 unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevScore;
switch ( statType )
{
case RankStat_Score:
break;
case RankStat_Kills:
unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgKills;
unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevKills;
break;
case RankStat_Damage:
unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgDamage;
unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevDamage;
break;
case RankStat_Healing:
unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgHealing;
unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevHealing;
break;
case RankStat_Support:
unStatAvg = m_vDailyStatsRankData[nRankIndex].nAvgSupport;
unStatStdDev = m_vDailyStatsRankData[nRankIndex].nStDevSupport;
break;
default:
Assert( 0 );
return 0;
}
if ( !unStatAvg || !unStatStdDev )
return 0;
int nMedalRank = StatMedal_None;
// Non-zero value?
if ( unStatAvg && unStatStdDev )
{
int nDelta = nValue - unStatAvg;
if ( nDelta > 0 )
{
float flPercentile = NormalDistributionCDF( (float) nValue, (float) unStatAvg, (float) unStatStdDev );
if ( flPercentile >= m_flGoldPercentile )
{
nMedalRank = StatMedal_Gold;
}
else if ( flPercentile >= m_flSilverPercentile )
{
nMedalRank = StatMedal_Silver;
}
else if ( flPercentile >= m_flBronzePercentile )
{
nMedalRank = StatMedal_Bronze;
}
// TODO:
// - Stat must be "n" std deviations above the match average, too (anti-farming)
// - Match must qualify:
// - Less than "n" minutes
// - At least "x" of "y" players at match end (no leavers?)
}
}
return clamp( nMedalRank, StatMedal_None, StatMedal_Gold );
}
float CMatchInfo::NormalDistributionCDF( float flValue, float flMu, float flSigma )
{
if ( flSigma <= 0.f )
return 0.5f;
return 0.5f * ( 1.f + erf( ( flValue - flMu ) / ( flSigma * sqrt( 2.f ) ) ) );
}
//-----------------------------------------------------------------------------
// CGCCompetitiveDailyStatsRollupJob
//-----------------------------------------------------------------------------
class CGCCompetitiveDailyStatsRollupJob : public GCSDK::CGCClientJob
{
public:
CGCCompetitiveDailyStatsRollupJob( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) {}
virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
{
GCSDK::CProtoBufMsg< CMsgGC_DailyCompetitiveStatsRollup_Response > msg( pNetPacket );
CMatchInfo *pInfo = GTFGCClientSystem()->GetMatch();
if ( !pInfo )
return false;
// Empty rankdata is valid (GC runs checks that might cause this as people reach new ranks)
for ( int i = 0; i < msg.Body().rankdata_size(); i++ )
{
CMatchInfo::DailyStatsRankBucket_t rankBucket = {
msg.Body().rankdata( i ).rank(),
msg.Body().rankdata( i ).records(),
msg.Body().rankdata( i ).avg_score(),
msg.Body().rankdata( i ).stdev_score(),
msg.Body().rankdata( i ).avg_kills(),
msg.Body().rankdata( i ).stdev_kills(),
msg.Body().rankdata( i ).avg_damage(),
msg.Body().rankdata( i ).stdev_damage(),
msg.Body().rankdata( i ).avg_healing(),
msg.Body().rankdata( i ).stdev_healing(),
msg.Body().rankdata( i ).avg_support(),
msg.Body().rankdata( i ).stdev_support()
};
pInfo->SetDailyRankData( rankBucket );
}
return true;
}
};
GC_REG_JOB( GCSDK::CGCClient, CGCCompetitiveDailyStatsRollupJob, "CGCCompetitiveDailyStatsRollupJob", k_EMsgGC_DailyCompetitiveStatsRollup_Response, k_EServerTypeGCClient );
//-----------------------------------------------------------------------------
// CGCVoteSystemVoteKickResponse
//-----------------------------------------------------------------------------
class CGCVoteSystemVoteKickResponse : public GCSDK::CGCClientJob
{
public:
CGCVoteSystemVoteKickResponse( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) {}
virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
{
GCSDK::CProtoBufMsg< CMsgGC_VoteKickPlayerRequestResponse > msg( pNetPacket );
if ( g_voteController )
{
g_voteController->GCResponseReceived( msg.Body().allowed() );
}
return true;
}
};
GC_REG_JOB( GCSDK::CGCClient, CGCVoteSystemVoteKickResponse, "CGCVoteSystemVoteKickResponse", k_EMsgGCVoteKickPlayerRequestResponse, k_EServerTypeGCClient );
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
class CGCKickPlayerFromLobbyJob : public GCSDK::CGCClientJob
{
public:
CGCKickPlayerFromLobbyJob( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {}
virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
{
GCSDK::CProtoBufMsg<CMsgGC_KickPlayerFromLobby> msg( pNetPacket );
CSteamID steamID( msg.Body().targetid() );
if ( steamID.IsValid() )
{
GTFGCClientSystem()->EjectMatchPlayer( steamID, TFMatchLeaveReason_ADMIN_KICK );
}
return true;
}
};
GC_REG_JOB( GCSDK::CGCClient, CGCKickPlayerFromLobbyJob, "CGCKickPlayerFromLobbyJob", k_EMsgGC_KickPlayerFromLobby, GCSDK::k_EServerTypeGCClient );
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CTFGCServerSystem::CTFGCServerSystem()
: m_flTimeRequestedLateJoin( -1.f )
, m_bLateJoinEligible( false )
, m_iSavedVisibleMaxPlayers( -1 )
, m_bOverridingVisibleMaxPlayers( false )
, m_bWaitingForNewMatchID( false )
, m_flWaitingForNewMatchTime( 0.f )
{
// replace base GCClientSystem
SetGCClientSystem( this );
m_unGameStartTime = 0;
m_bSetupSchema = false;
m_timeLastSendGameServerInfoAndConnectedPlayers = 0;
//m_flUpdateGCGameTime = 0;
//m_nUploadingMatchStats = EDOTA_MATCH_STATS_IDLE;
//m_nParentRelayCount = 0;
//m_nLastUpdateGCServerType = -1;
m_eLastGameServerUpdateState = ServerMatchmakingState_NOT_PARTICIPATING;
m_eLastGameServerUpdateMatchmakingMode = TF_Matchmaking_MVM;
m_nLastGameServerUpdateBotCount = -1;
m_nLastGameServerUpdateMaxHumans = -1;
m_nLastGameServerUpdateSlotsFree = -1;
m_nLastGameServerUpdateLobbyMMVersion = 0;
m_flTimeBecameEmptyWithLobby = 0.0f;
m_timeLastConnectedToGC = 0.f;
m_pMatchInfo = NULL;
g_bWarnedAboutMaxplayersInMVM = false;
}
CTFGCServerSystem::~CTFGCServerSystem( void )
{
// Prevent other system from using this pointer after it's destroyed
SetGCClientSystem( NULL );
if ( m_pMatchInfo )
{
delete m_pMatchInfo;
}
}
bool CTFGCServerSystem::Init()
{
ListenForGameEvent( "player_disconnect" );
ListenForGameEvent( "player_score_changed" );
g_bWarnedAboutMaxplayersInMVM = false;
return true;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFGCServerSystem::PreInitGC()
{
BaseClass::PreInitGC();
if ( !m_bSetupSchema )
{
// REG_SHARED_OBJECT_SUBCLASS( CDOTAHeroStandings );
// REG_SHARED_OBJECT_SUBCLASS( CDOTAGameAccountClient );
REG_SHARED_OBJECT_SUBCLASS( CTFGSLobby );
REG_SHARED_OBJECT_SUBCLASS( CTFParty );
m_bSetupSchema = true;
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFGCServerSystem::PostInitGC()
{
BaseClass::PostInitGC();
}
//-----------------------------------------------------------------------------
void CTFGCServerSystem::LevelShutdownPostEntity()
{
BaseClass::LevelShutdownPostEntity();
}
//-----------------------------------------------------------------------------
void CTFGCServerSystem::Shutdown()
{
BaseClass::Shutdown();
// Remove listener, if we have one
if ( m_ourSteamID.IsValid() )
{
GCClientSystem()->GetGCClient()->RemoveSOCacheListener( m_ourSteamID, this );
}
}
void CTFGCServerSystem::LevelInitPreEntity()
{
BaseClass::LevelInitPreEntity();
// Assert( m_nUploadingMatchStats != EDOTA_MATCH_STATS_UPLOADING );
// if ( m_nUploadingMatchStats == EDOTA_MATCH_STATS_UPLOADING )
// {
// Warning( "Error, changed level while waiting for match stats to upload!\n" );
// return;
// }
// m_nUploadingMatchStats = EDOTA_MATCH_STATS_IDLE;
}
//-----------------------------------------------------------------------------
void CTFGCServerSystem::ClientActive( CSteamID steamIDClient )
{
if ( !steamIDClient.IsValid() || !steamIDClient.BIndividualAccount() )
{
if ( !HushAsserts() )
{
Assert( steamIDClient.IsValid() );
Assert( steamIDClient.BIndividualAccount() );
}
return;
}
CMatchInfo *pMatch = GetMatch();
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamIDClient ) : NULL;
if ( !pMatchPlayer )
return;
pMatchPlayer->OnActive();
// Only subscribe to match players' SOCaches. They're the only ones who will have
// parties that we care about.
GetGCClient()->AddSOCacheListener( steamIDClient, this );
}
//-----------------------------------------------------------------------------
void CTFGCServerSystem::ClientConnected( CSteamID steamIDClient, edict_t *pEntity )
{
// Note that we won't be notified of players connecting with unknown steamIDs, SteamIDAllowedToConnect() should be
// used to reject those in a strict MM scenario where that is not acceptable.
CMatchInfo *pMatch = GetMatch();
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamIDClient ) : NULL;
if ( !pMatchPlayer )
return;
pMatchPlayer->OnConnected( pEntity->m_EdictIndex );
}
//-----------------------------------------------------------------------------
void CTFGCServerSystem::ClientDisconnected( CSteamID steamIDClient )
{
if ( !steamIDClient.IsValid() || !steamIDClient.BIndividualAccount() )
{
Assert( steamIDClient.IsValid() );
Assert( steamIDClient.BIndividualAccount() );
return;
}
GetGCClient()->RemoveSOCacheListener( steamIDClient, this );
// This is here because ClientDisconnected code is not called on gamerules or player
// when the game is in state g_fGameOver. See CServerGameClients::ClientDisconnect.
CBasePlayer* pPlayer = UTIL_PlayerBySteamID( steamIDClient );
if ( TFGameRules() && pPlayer )
{
TFGameRules()->SetPlayerNextMapVote( pPlayer->entindex(), CTFGameRules::USER_NEXT_MAP_VOTE_UNDECIDED );
}
CMatchInfo *pMatch = GetMatch();
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamIDClient ) : NULL;
if ( !pMatchPlayer )
{
return;
}
if ( !pMatchPlayer->bConnected )
{
Assert( !"Player disconnecting is not marked connected" );
return;
}
// Did they disconnect while still loading in?
bool bWasActive = pMatchPlayer->nConnectingButNotActiveIndex == 0;
RTime32 now = CRTime::RTime32TimeCur();
// Time spent in the active state.
RTime32 timeSpentActive = bWasActive ? ( now - pMatchPlayer->rtLastActiveEvent ) : 0;
// Mark disconnected
pMatchPlayer->bConnected = false;
pMatchPlayer->nConnectingButNotActiveIndex = 0;
// If they were active, they now transitioned to inactive. If they were loading, this value is still the last time
// they went inactive, and shouldn't change.
if ( bWasActive )
{ pMatchPlayer->rtLastActiveEvent = now; }
// Optionally forgive some amount of their disconnected seconds accumulation based on how long they were present.
int nForgiveRatio = tf_mm_player_disconnect_time_forgive_ratio.GetInt();
if ( timeSpentActive > 0 && nForgiveRatio > 0 && pMatchPlayer->nDisconnectedSeconds > 0 )
{
double dForgiven = (double)pMatchPlayer->nDisconnectedSeconds - ( (double)timeSpentActive / nForgiveRatio );
int nOldVal = pMatchPlayer->nDisconnectedSeconds;
pMatchPlayer->nDisconnectedSeconds = Max( 0, (int)dForgiven );
MMLog("Client %s was connected for %u seconds, disconnect timer lowered from %i to %i\n",
steamIDClient.Render(), timeSpentActive, nOldVal, pMatchPlayer->nDisconnectedSeconds );
}
}
//-----------------------------------------------------------------------------
void CTFGCServerSystem::PreClientUpdate( )
{
BaseClass::PreClientUpdate();
CRTime::UpdateRealTime();
if ( GCClientSystem()->BConnectedtoGC() )
{
m_timeLastConnectedToGC = Plat_FloatTime();
}
// We want a pause so players can read what the next map is. Once we've waited
// long enough, we're doing a map change regardless of if the GC got back to us
// with a new match ID.
if ( Plat_FloatTime() > m_flWaitingForNewMatchTime
&& m_flWaitingForNewMatchTime != 0.f )
{
LaunchNewMatchForLobby();
}
//
// Check for updating the caches that we're listening to
//
CSteamID const *pSteamID = engine->GetGameServerSteamID();
if ( pSteamID && m_ourSteamID != *pSteamID )
{
Assert( pSteamID->BGameServerAccount() );
// If we were previously listening to somebody else, stop listening. This
// means we were connected, then reconnected and got a different Steam ID,
// and is weird, but possible
if ( m_ourSteamID.IsValid() )
{
MMLog( "CTFGCServerSystem - removing listener to old Steam ID %s\n", m_ourSteamID.Render() );
GCClientSystem()->GetGCClient()->RemoveSOCacheListener( m_ourSteamID, this );
}
// Remember our new Steam ID
m_ourSteamID = *pSteamID;
// And start listening
GCClientSystem()->GetGCClient()->AddSOCacheListener( m_ourSteamID, this );
}
MatchPlayerAbandonThink();
UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_None, false );
// Check if the game is empty, and we need to shut down our lobby
CTFGSLobby *pLobby = GetLobby();
if ( pLobby )
{
switch ( pLobby->GetState() )
{
case CSOTFGameServerLobby_State_SERVERSETUP:
// We could most definitely be empty here, waiting for players to join!
// Don't kill the server just yet
break;
case CSOTFGameServerLobby_State_RUN:
break;
default:
case CSOTFGameServerLobby_State_UNKNOWN:
MMLog( "Lobby in invalid state %d\n", (int)pLobby->GetState() );
break;
}
}
// Check for slamming visiblemaxplayers
static ConVarRef sv_visiblemaxplayers( "sv_visiblemaxplayers" );
if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
{
// Abort the server if they don't have enough maxplayers
if ( gpGlobals->maxClients < 32 )
{
if( !g_bWarnedAboutMaxplayersInMVM )
{
// Prevent this warning from endlessly spamming the console...
g_bWarnedAboutMaxplayersInMVM = true;
Warning( "You must set maxplayers to 32 to host Mann vs. Machine\n" );
}
if ( engine->IsDedicatedServer() )
{
engine->ServerCommand( "exit\n" );
}
return;
}
// This changes what the server browser displays
// update sv_visiblemaxplayers for MvM, count only non-bot spectators
CUtlVector<CTFPlayer *> spectatorVector;
CollectPlayers( &spectatorVector, TEAM_SPECTATOR );
int spectatorCount = 0;
FOR_EACH_VEC ( spectatorVector, iIndex )
{
if ( !spectatorVector[iIndex]->IsBot() && !spectatorVector[iIndex]->IsReplay() && !spectatorVector[iIndex]->IsHLTV() )
{
spectatorCount++;
}
}
int playerCount = kMVM_DefendersTeamSize + spectatorCount;
if ( sv_visiblemaxplayers.GetInt() <= 0 || sv_visiblemaxplayers.GetInt() != playerCount )
{
MMLog( "Setting sv_visiblemaxplayers to %d for MvM\n", playerCount );
// save off visible players
if ( !m_bOverridingVisibleMaxPlayers )
{
m_bOverridingVisibleMaxPlayers = true;
m_iSavedVisibleMaxPlayers = sv_visiblemaxplayers.GetInt();
}
sv_visiblemaxplayers.SetValue( playerCount );
}
}
else
{
// Not in MvM. Check for restoring sv_visiblemaxplayers
if ( m_bOverridingVisibleMaxPlayers )
{
MMLog( "Restoring sv_visiblemaxplayers to %d\n", m_iSavedVisibleMaxPlayers );
sv_visiblemaxplayers.SetValue( m_iSavedVisibleMaxPlayers );
m_bOverridingVisibleMaxPlayers = false;
m_iSavedVisibleMaxPlayers = -1;
}
}
// You may not be in matchmaking if you have a password!
static ConVarRef sv_password( "sv_password" );
if ( tf_mm_servermode.GetInt() != 0 && *sv_password.GetString() != '\0' )
{
Warning( "Setting tf_mm_servermode=0 due to sv_password\n" );
tf_mm_servermode.SetValue( 0 );
}
// TFGameRules()->SetStableMode( IsStableMode() );
//
// if ( HLTVDirector() && HLTVDirector()->GetHLTVServer() )
// {
// gcGameTime = Max( 0.0f, TFGameRules()->GetDOTATime() - HLTVDirector()->GetDelay() );
// }
// else
// {
// gcGameTime = TFGameRules()->GetDOTATime();
// }
// // Slam server region to 255 while in PVE mode
// static ConVarRef sv_region( "sv_region" );
// if ( sv_region.GetInt() != 255 )
// {
// MMLog( "Setting 'sv_region 255 ' due to tf_mm_servermode\n" );
// sv_region.SetValue( 255 );
// }
}
void CTFGCServerSystem::MatchPlayerAbandonThink()
{
CMatchInfo *pMatchInfo = GetMatch();
if ( !pMatchInfo || pMatchInfo->m_bMatchEnded )
{ return; }
int nAbandonSeconds = tf_mm_player_disconnect_time_before_abandon.GetInt();
// Disabled
if ( nAbandonSeconds < 0 )
{ return; }
int nPlayers = pMatchInfo->GetNumTotalMatchPlayers();
bool bDroppedPlayers = false;
for ( int idx = 0; idx < nPlayers; idx++ )
{
CMatchInfo::PlayerMatchData_t *pPlayer = pMatchInfo->GetMatchDataForPlayer( idx );
// The engine doesn't really tell the game of connected-but-not-active players dropping. Keep an eye on their
// entity being quietly cleaned up and note the disconnect.
if ( pPlayer->nConnectingButNotActiveIndex )
{
const CSteamID *pIndexSteamID = engine->GetClientSteamIDByPlayerIndex( pPlayer->nConnectingButNotActiveIndex );
if ( !pIndexSteamID || *pIndexSteamID != pPlayer->steamID )
{
MMLog( "Match player %s dropped before going active\n", pPlayer->steamID.Render() );
ClientDisconnected( pPlayer->steamID );
}
}
if ( !pPlayer->bConnected && !pPlayer->bDropped )
{
// nDisconnectedSeconds is accumulated from previous absences, but doesn't include the current disconnect.
int nTimeGone = CRTime::RTime32TimeCur() - pPlayer->rtLastActiveEvent + pPlayer->nDisconnectedSeconds;
if ( nTimeGone > nAbandonSeconds )
{
MMLog( "Match player %s has been absent for a combined total of %u seconds, dropping from match\n",
pPlayer->steamID.Render(), nTimeGone );
SetMatchPlayerDropped( pPlayer->steamID, pPlayer->bEverConnected ? TFMatchLeaveReason_AWOL : TFMatchLeaveReason_NO_SHOW );
bDroppedPlayers = true;
}
}
}
if ( bDroppedPlayers )
{ UpdateServerDetails(); }
}
//-----------------------------------------------------------------------------
bool CTFGCServerSystem::EjectMatchPlayer( CSteamID steamID, TFMatchLeaveReason eReason )
{
CMatchInfo *pMatch = GetLiveMatch();
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamID ) : NULL;
if ( !pMatchPlayer || pMatchPlayer->bDropped )
{ return false; }
SetMatchPlayerDropped( steamID, eReason );
KickRemovedMatchPlayer( steamID );
return true;
}
//-----------------------------------------------------------------------------
void CTFGCServerSystem::MatchPlayerVoteKicked( CSteamID steamID )
{
bool bEjected = EjectMatchPlayer( steamID, TFMatchLeaveReason_VOTE_KICK );
if ( bEjected )
{
// Was part of our match, handled.
MMLog( "Player %s vote-kicked from live match\n", steamID.Render() );
return;
}
// Not part of our match, check if they used to be
CMatchInfo *pMatch = GetLiveMatch();
if ( !pMatch )
return;
CMatchInfo::PlayerMatchData_t *pPlayer = pMatch->GetMatchDataForPlayer( steamID );
if ( !pPlayer || ( pPlayer && !pPlayer->bDropped ) )
{
AssertMsg( !pPlayer || pPlayer->bDropped,
"Player is still part of our match, so EjectMatchPlayer should have succeeded" );
return;
}
// Previously in this match, but left before kick arrived. Send this message made just for that occasion, update our
// record to reflect the reason.
MMLog( "Player %s vote-kicked after departing match\n", steamID.Render() );
pPlayer->eDropReason = TFMatchLeaveReason_VOTE_KICK;
ReliableMsgPlayerVoteKickedAfterLeavingMatch *pReliable = new ReliableMsgPlayerVoteKickedAfterLeavingMatch();
auto &msg = pReliable->Msg().Body();
msg.set_steam_id( steamID.ConvertToUint64() );
msg.set_lobby_id( pMatch->m_nLobbyID );
msg.set_match_id( pMatch->m_nMatchID );
pReliable->Enqueue();
}
//-----------------------------------------------------------------------------
bool CTFGCServerSystem::KickRemovedMatchPlayer( CSteamID steamIDClient )
{
CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerBySteamID( steamIDClient ) );
if ( !pPlayer )
{ return false; }
MMLog( "Kicking ejected player %s\n", steamIDClient.Render() );
engine->ServerCommand( UTIL_VarArgs( "kickid %d %s\n", pPlayer->GetUserID(), "#TF_MM_Generic_Kicked" ) );
return true;
}
//-----------------------------------------------------------------------------
bool CTFGCServerSystem::CanChangeMatchPlayerTeams()
{
// Warning: LaunchNewMatchForLobby is counting on being able to do this, so avoid the temptation to forbid this
// during match-result phase or similar (this is only for is-our-state-consistent-to-allow-this, not
// should-gamerules-be-doing-this, that's on them)
CMatchInfo *pMatch = GetMatch();
const IMatchGroupDescription* pMatchDesc = pMatch ? GetMatchGroupDescription( pMatch->m_eMatchGroup ) : NULL;
if ( !pMatch || !pMatchDesc || pMatch->BMatchTerminated() || !pMatchDesc->BCanServerChangeMatchPlayerTeams() )
{ return false; }
// If we're waiting to launch a new match, the team change would be for the new match that the GC is about to send
// down, which has new teams. We probably are not intending that since we have no idea what this player's current
// team is.
//
// (See the Team Assignments comment at the start of this file for ordering regarding new matches and team changes.)
if ( BPendingNewMatch() )
{ return false; }
return true;
}
//-----------------------------------------------------------------------------
// ChangeMatchPlayerTeams handling
//-----------------------------------------------------------------------------
void CTFGCServerSystem::ChangeMatchPlayerTeam( CSteamID steamID, TF_GC_TEAM eTeam )
{
// Helper for single member.
CUtlVectorFixed< PlayerTeamPair_t, 1 > vec;
vec.AddToTail( { steamID, eTeam } );
ChangeMatchPlayerTeams( vec );
}
template< typename ANY_ALLOCATOR >
void CTFGCServerSystem::ChangeMatchPlayerTeams( const CUtlVector< PlayerTeamPair_t, ANY_ALLOCATOR > &vecNewTeams )
{
if ( !CanChangeMatchPlayerTeams() )
{
// Some match logic is badly out of sync if it thinks it can do this.
MMLog( "!! Game server is attempting to change player teams in an invalidate state\n" );
AbortInvalidMatchState();
return;
}
// Job takes ownership of message
MMLog( "Sending team assignment request to GC:\n" );
ReliableMsgChangeMatchPlayerTeams *pReliable = new ReliableMsgChangeMatchPlayerTeams();
auto &msg = pReliable->Msg().Body();
msg.set_match_id( GetMatch()->m_nMatchID );
msg.set_lobby_id( GetMatch()->m_nLobbyID );
FOR_EACH_VEC( vecNewTeams, idx )
{
const CSteamID &steamID = vecNewTeams[idx].steamID;
const TF_GC_TEAM &eTeam = vecNewTeams[idx].eTeam;
// Do we know about this guy?
CMatchInfo::PlayerMatchData_t *pPlayer = m_pMatchInfo->GetMatchDataForPlayer( steamID );
if ( !pPlayer || pPlayer->bDropped )
{
MMLog("!! Got team change request for player not in match %s\n", steamID.Render() );
continue;
}
MMLog(" %37s -> %d\n", steamID.Render(), eTeam );
auto *member = msg.add_member();
member->set_member_id( steamID.ConvertToUint64() );
member->set_new_team( eTeam );
// Reflect change locally immediately, this message should not fail
pPlayer->eGCTeam = eTeam;
}
pReliable->Enqueue();
}
void CTFGCServerSystem::ChangeMatchPlayerTeamsResponse( bool bSuccess )
{
if ( !bSuccess && GetLobby() )
{
// If the lobby went away prior to the GC responding, it is out of sync and can't do anything meaningful with
// these updates right now, but we still have authority to finish the match and send a result, so just keep
// plugging along. But if we still HAVE the lobby, and the GC said no, something is badly out of sync with this
// match.
MMLog( "!! ChangeMatchPlayerTeams rejected, something is confused\n" );
AbortInvalidMatchState();
return;
}
MMLog( "ChangeMatchPlayerTeams acknowledged\n" );
}
//-----------------------------------------------------------------------------
const MapDef_t* CTFGCServerSystem::GetNextMapVoteByIndex( int nIndex ) const
{
const CTFGSLobby *pLobby = GetLobby();
if ( pLobby && nIndex < pLobby->Obj().next_maps_for_vote_size() )
{
return GetItemSchema()->GetMasterMapDefByIndex( pLobby->Obj().next_maps_for_vote( nIndex ) );
}
Assert( false );
return GetItemSchema()->GetMasterMapDefByName( "ctf_2fort" );
}
//-----------------------------------------------------------------------------
// Purpose: GC Msg to request starting a new match for an existing lobby
//-----------------------------------------------------------------------------
void CTFGCServerSystem::NewMatchForLobbyResponse( bool bSuccess )
{
// We should be expecting this
if ( !m_bWaitingForNewMatchID )
{
MMLog( "!! Got a NewMatchForLobbyResponse when not expecting it\n" );
AbortInvalidMatchState();
}
Assert( TFGameRules() );
MMLog( "NewMatchID response recieved -- %s.\n", bSuccess ? "Success!" : "Failed!" );
m_bWaitingForNewMatchID = false;
CMatchInfo *pMatch = GetMatch();
if ( pMatch && pMatch->m_bServerCreated )
{
// We went ahead without a match ID, the new ID should've already arrived in SOUpdated
if ( bSuccess )
{
if ( !pMatch || pMatch->m_bServerCreated || !pMatch->m_nMatchID )
{
MMLog( "!! Got a NewMatchForLobby response but have not received a new match ID" );
AbortInvalidMatchState();
}
}
else
{
// Failed, but we already have a running speculative match. It is essentially an unofficial match now.
MMLog( "!! NewMatchForLobby responded negatively, this match will likely not be acknowledged by the system.\n" );
// TODO ROLLING MATCHES: Check that the jobs that will now send MatchID 0 do something salient
}
}
else
{
// Still waiting to actually kick off the new match. If the response was a failure, we can just abort.
if ( !bSuccess )
{
MMLog( "!! NewMatchForLobby responded negatively. We haven't launched the match yet, so just shutting down.\n" );
if ( TFGameRules() )
{
TFGameRules()->KickPlayersNewMatchIDRequestFailed();
}
else
{
AbortInvalidMatchState();
}
}
}
}
bool CTFGCServerSystem::CanRequestNewMatchForLobby()
{
// If this is a match that is not in sync with the GC, or it's not even a match, then no
if ( !m_pMatchInfo || !GetLobby() || m_pMatchInfo->BMatchTerminated() )
{ return false; }
// If we're waiting on other pending match magic, then no you can't stack them god help your soul.
if ( m_pMatchInfo->m_bServerCreated || m_bWaitingForNewMatchID || m_flWaitingForNewMatchTime != 0.f )
{ return false; }
// Match description allow it?
const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( m_pMatchInfo->m_eMatchGroup );
if ( !pMatchDesc->BCanServerRequestNewMatchForLobby() )
{ return false; }
return true;
}
void CTFGCServerSystem::RequestNewMatchForLobby( const MapDef_t* pNewMap )
{
// Wat r u doin
if ( !CanRequestNewMatchForLobby() )
{
AbortInvalidMatchState();
}
m_flWaitingForNewMatchTime = Plat_FloatTime() + tf_mm_next_map_result_hold_time.GetFloat();
m_bWaitingForNewMatchID = true;
m_pMatchInfo->m_strMapName = pNewMap->pszMapName;
ReliableMsgNewMatchForLobby *pReliable = new ReliableMsgNewMatchForLobby();
auto &msg = pReliable->Msg().Body();
msg.set_next_map_id( pNewMap->m_nDefIndex );
msg.set_lobby_id( GetLobby()->GetGroupID() );
msg.set_current_match_id( GetMatch()->m_nMatchID );
MMLog( "Sending request to GC for a new match ID.\n" );
pReliable->Enqueue();
}
//-----------------------------------------------------------------------------
void CTFGCServerSystem::SetMatchPlayerDropped( CSteamID steamID, TFMatchLeaveReason eReason )
{
CMatchInfo *pMatch = GetMatch();
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamID ) : NULL;
Assert( pMatchPlayer );
if ( !pMatchPlayer )
{ return; }
Assert( !pMatchPlayer->bDropped );
// Determine if this was an abandon
bool bAbandon = true;
switch ( eReason )
{
case TFMatchLeaveReason_VOTE_KICK:
// Vote kicks don't penalize you currently. We need to revisit how these tie in with e.g. abuse reports/etc..
bAbandon = false;
break;
case TFMatchLeaveReason_NO_SHOW:
case TFMatchLeaveReason_GC_REMOVED:
// For right now, until we have more confidence in our network connectivity and possibly have SDR hooked up,
// we'll give no shows the benefit of the doubt if they never made it to connect. ( If they can't connect an
// give up and click abandon on their end, it will show up as GC_REMOVED )
bAbandon = pMatchPlayer->bEverConnected;
break;
case TFMatchLeaveReason_ADMIN_KICK:
case TFMatchLeaveReason_AWOL:
case TFMatchLeaveReason_IDLE:
break;
default: AssertMsg( false, "Unhandled TFMatchLeaveReason" );
}
bAbandon = bAbandon && !pMatch->BPlayerSafeToLeaveMatch( steamID );
/// TODO ROLLING MATCHES: Technically if this happens with a rolling match in queue, we'll drop them from the old
/// match without record of them in the new?
pMatch->DropPlayer( steamID, eReason, bAbandon );
SendPlayerLeftMatch( steamID, eReason, bAbandon );
}
void CTFGCServerSystem::UpdateServerDetails(void)
{
UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_None, false );
}
bool CTFGCServerSystem::ShouldHibernate()
{
// We only hibernate if we're just sitting there with a freshly loaded map
return engine->IsDedicatedServer() && tf_allow_server_hibernation.GetBool() && !GetLobby() && !BPendingReliableMessages() && !m_pMatchInfo;
}
void CTFGCServerSystem::FireGameEvent( IGameEvent *event )
{
// Disconnected from gameserver
if ( !Q_stricmp( event->GetName(), "player_disconnect" ) )
{
const char * pszReason = event->GetString( "reason", "" );
if ( Q_strstr( pszReason, "kick" ) || Q_strstr( pszReason, "Kick" ) || Q_strstr( pszReason, g_pszVoteKickString ) )
{
CBasePlayer *pPlayer = UTIL_PlayerByUserId( event->GetInt( "userid", 0 ) );
if ( !pPlayer )
return;
CSteamID steamId;
if ( !pPlayer->GetSteamID( &steamId ) )
return;
// Only care if this is a member of a live match
CMatchInfo *pMatch = GetMatch();
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch ? pMatch->GetMatchDataForPlayer( steamId ) : NULL;
if ( !pMatch || !pMatchPlayer || pMatch->m_bMatchEnded || pMatchPlayer->bDropped )
{ return; }
TFMatchLeaveReason eReason = TFMatchLeaveReason_ADMIN_KICK;
if ( Q_strstr( pszReason, g_pszIdleKickString ) )
{
eReason = TFMatchLeaveReason_IDLE;
}
// kickid %d You have been voted off;
// Vote kicks should not trigger abandon
else if ( Q_strstr( pszReason, g_pszVoteKickString ) )
{
eReason = TFMatchLeaveReason_VOTE_KICK;
}
SetMatchPlayerDropped( steamId, eReason );
UpdateServerDetails();
}
}
else if ( FStrEq( event->GetName(), "player_score_changed" ) )
{
CMatchInfo *pMatch = GetMatch();
if ( !pMatch )
return;
CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( event->GetInt( "player" ) ) );
if ( !pPlayer )
return;
CSteamID steamId;
if ( !pPlayer->GetSteamID( &steamId ) )
return;
const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( pMatch->m_eMatchGroup );
if ( !pMatchDesc || !pMatchDesc->m_pProgressionDesc )
return;
// Add to this player's score XP
pMatch->GiveXPRewardToPlayerForAction( steamId, CMsgTFXPSource_XPSourceType_SOURCE_SCORE, event->GetInt( "delta", 0 ) );
}
}
CTFParty* CTFGCServerSystem::GetPartyForPlayer( CSteamID steamID ) const
{
// Dig up this guy's party
CGCClientSharedObjectCache* pSOCache = const_cast< CTFGCServerSystem* >( this )->GetSOCache( steamID );
if ( !pSOCache )
{
return NULL;
}
CSharedObjectTypeCache* pPartyTypeCache = pSOCache->FindTypeCache( CTFParty::k_nTypeID );
if ( !pPartyTypeCache || pPartyTypeCache->GetCount() == 0 )
{
return NULL;
}
return assert_cast< CTFParty* >( pPartyTypeCache->GetObject( 0 ) );
}
const CMatchInfo::PlayerMatchData_t *CTFGCServerSystem::GetLiveMatchPlayer( CSteamID steamID ) const
{
return const_cast<CTFGCServerSystem*>(this)->GetLiveMatchPlayer( steamID );
}
CMatchInfo::PlayerMatchData_t *CTFGCServerSystem::GetLiveMatchPlayer( CSteamID steamID )
{
CMatchInfo *pMatch = GetMatch();
if ( !pMatch || pMatch->m_bMatchEnded )
{ return NULL; }
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch->GetMatchDataForPlayer( steamID );
if ( !pMatchPlayer || pMatchPlayer->bDropped )
{ return NULL; }
return pMatchPlayer;
}
void CTFGCServerSystem::SOCreated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent )
{
// Msg( "CTFGCServerSystem::SOCreated type = %d owner = %s\n", pObject->GetTypeID(), steamIDOwner.Render() );
// Lobby handling
if ( pObject->GetTypeID() == CTFGSLobby::k_nTypeID )
{
const CTFGSLobby *pConstLobby = static_cast<const CTFGSLobby*>( pObject );
CTFGSLobby *pLobby = const_cast<CTFGSLobby *>( pConstLobby ); // GROSS
Assert( pLobby == GetLobby() ); // There can be only be one...
MMLog( "Lobby %016llx instanced on this server in state %s\n",
pLobby->GetGroupID(), CSOTFGameServerLobby_State_Name( pLobby->GetState() ).c_str() );
// Check if we need to switch the map or load a pop file.
CMsgGameServerMatchmakingStatus_Event statusEvent = CMsgGameServerMatchmakingStatus_Event_None;
bool bNewLobby = ( pLobby->GetState() == CSOTFGameServerLobby_State_SERVERSETUP );
if ( m_bMMServerMode && bNewLobby )
{
MMLog( " Map: '%s'\n", pLobby->GetMapName() );
MMLog( " Mission: '%s'\n", pLobby->GetMissionName() );
EMatchGroup eMatchGroup = (EMatchGroup)pLobby->Obj().match_group();
// Acknowledge the players that just connected. (This will create
// reservations for the players and let the GC we are expecting the
// players.)
statusEvent = CMsgGameServerMatchmakingStatus_Event_AcknowledgePlayers;
// Create a record of the match on first connect.
if ( m_pMatchInfo )
{
MMLog( "!! Received new anticipated lobby while running existing match. "
"Old match ID [ %llu ] ended [ %u ] "
"New matchID [ %llu ]\n",
m_pMatchInfo->m_nMatchID, m_pMatchInfo->m_bMatchEnded,
pLobby->GetMatchID() );
Assert( false );
delete m_pMatchInfo;
// In theory the overwritten match will now be forgotten by us, all errant players kicked by the
// UpdateConnectedPlayers tick...
}
m_pMatchInfo = new CMatchInfo( pLobby );
GTFGCClientSystem()->DumpLobby();
if ( eMatchGroup == EMatchGroup::k_nMatchGroup_Invalid ||
!GetMatchGroupDescription( eMatchGroup )->InitServerSettingsForMatch( pConstLobby ) )
{
AbortInvalidMatchState();
}
// FIXME We should have some version checking like this.
// int engineServerVersion = engine->GetServerVersion();
//
// // Version checking is enforced if both sides do not report zero as their version
// if ( engineServerVersion && g_gcServerVersion && engineServerVersion != g_gcServerVersion )
// {
// // If we're out of date exit
// Msg("Version out of date (GC wants %d, we are %d), terminating!\n", g_gcServerVersion, engine->GetServerVersion() );
// engine->ServerCommand( "quit\n" );
// }
}
else
{
// We could've just gotten re-sent this lobby, is it the match we think we're running? If we are running a
// match for a different lobby, something is super wrong
uint64 nExistingMatchID = m_pMatchInfo ? m_pMatchInfo->m_nMatchID : 0;
uint64 nLobbyMatchID = pLobby->Obj().has_match_id() ? pLobby->GetMatchID() : 0;
if ( m_pMatchInfo && nExistingMatchID == nLobbyMatchID )
{
MMLog( "GC refreshed lobby for match ID [ %llu ]\n", m_pMatchInfo->m_nMatchID );
}
else
{
MMLog( "!! Got assigned a lobby not in server-setup state, or when not accepting lobbies. Rejecting.\n"
"Lobby matchID [ %llu ], existing match [ %llu ]\n",
pLobby->GetMatchID(), m_pMatchInfo ? m_pMatchInfo->m_nMatchID : 0ull );
if ( !m_pMatchInfo )
{
// Not running a match, don't want this one, just reject the lobby.
//
// This can happen when we crash and are handed a stale lobby upon reboot, rejecting it will
// terminate that match.
SendRejectLobby();
}
else
{
// Otherwise, we thought we had a lobby, but the GC sent us a different match? No idea what is going
// on, probably some bad de-sync happened.
//
// No faith we can continue and send authoritative match results about anything.
AbortInvalidMatchState();
}
}
}
UpdateConnectedPlayersAndServerInfo( statusEvent, false );
}
}
void CTFGCServerSystem::SOUpdated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent )
{
// Don't care if we're not running a match
CMatchInfo *pMatch = GetMatch();
if ( !pMatch )
return;
// Lobby handling
if ( pObject->GetTypeID() == CTFGSLobby::k_nTypeID )
{
const CTFGSLobby *pConstLobby = static_cast<const CTFGSLobby*>( pObject );
CTFGSLobby *pLobby = const_cast<CTFGSLobby *>( pConstLobby ); // GROSS
Assert( pLobby == GetLobby() ); // There can be only be one...
bool bNeedsToUpdatePlayerAndServer = false;
// Check if we have new reservations not part of the match
for ( int i = 0; i < pLobby->GetNumMembers(); i++ )
{
const CTFLobbyMember *pMemberDetails = pLobby->GetMemberDetails( i );
Assert( pMemberDetails );
if ( !pMemberDetails )
continue;
CSteamID steamID( pMemberDetails->id() );
CTFLobbyMember_ConnectState eLobbyState = pLobby->GetMemberConnectState( i );
if ( eLobbyState == CTFLobbyMember_ConnectState_RESERVATION_PENDING )
{
CMatchInfo::PlayerMatchData_t *pPlayer = pMatch->GetMatchDataForPlayer( pLobby->GetMember( i ) );
if ( !pPlayer || pPlayer->bDropped )
{
// Lobby has a new player we don't think is in our match, force an update to acknowledge them ASAP
bNeedsToUpdatePlayerAndServer = true;
}
}
}
if ( bNeedsToUpdatePlayerAndServer )
{
UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_AcknowledgePlayers, true );
}
// If we terminated while the new match ID was pending we're still unwinding the incoming messages
bool bNewMatchID = m_pMatchInfo && !m_pMatchInfo->BMatchTerminated() && ( m_pMatchInfo->m_nMatchID != pLobby->GetMatchID() );
if ( bNewMatchID )
{
if ( m_bWaitingForNewMatchID && m_pMatchInfo->m_bServerCreated )
{
// We sent a request for a new matchID to put in for the match
// we're running, and it just came back.
MMLog( "Received new matchID for server-created match. "
"New matchID [ %llu ]\n",
pLobby->GetMatchID() );
m_pMatchInfo->m_nMatchID = pLobby->GetMatchID();
m_pMatchInfo->m_bServerCreated = false;
}
else if ( m_bWaitingForNewMatchID && m_flWaitingForNewMatchTime != 0.f )
{
// We're counting down to launching a new match, and the new match ID arrived. We'll pick it up from the
// lobby in LaunchNewMatchForLobby
MMLog( "Received new matchID while waiting for new matchID. "
"Old match ID [ %llu ] ended [ %u ] "
"New matchID [ %llu ]\n",
m_pMatchInfo->m_nMatchID, m_pMatchInfo->m_bMatchEnded,
pLobby->GetMatchID() );
}
else if ( !m_bWaitingForNewMatchID && m_flWaitingForNewMatchTime == 0.f )
{
// A lobby came in with a match ID that's not what our current
// one is, and we were not expecting this.
//
// Note that we hold on to the stale lobby between NewMatchForLobby and LaunchNewMatchForLobby, so we
// don't panic if the stale lobby updates. The only other way out of that state is terminating the
// match.
MMLog( "Received new matchID when we weren't expecting one! "
"Current matchID [ %llu ] "
"New matchID [ %llu ]\n",
m_pMatchInfo->m_nMatchID,
pLobby->GetMatchID() );
AbortInvalidMatchState();
}
}
}
}
void CTFGCServerSystem::SODestroyed( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent )
{
// Lobby handling
if ( pObject->GetTypeID() == CTFGSLobby::k_nTypeID )
{
// Lobby is gone! Reset
UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_None, true );
}
}
const CTFGSLobby *CTFGCServerSystem::GetLobby() const
{
if ( !m_ourSteamID.IsValid() )
return NULL;
GCSDK::CGCClientSharedObjectCache *pSOCache = GCClientSystem()->GetSOCache( m_ourSteamID );
if ( !pSOCache )
return NULL;
CSharedObjectTypeCache *pTypeCache = pSOCache->FindBaseTypeCache( CTFGSLobby::k_nTypeID );
if ( pTypeCache && pTypeCache->GetCount() > 0 )
{
AssertMsg1( pTypeCache->GetCount() == 1, "Server has %d lobby objects in his cache! He should only have 1.", pTypeCache->GetCount() );
const CTFGSLobby *pLobby = static_cast<CTFGSLobby*>( pTypeCache->GetObject( pTypeCache->GetCount() - 1 ) );
return pLobby;
}
return NULL;
}
CTFGSLobby *CTFGCServerSystem::GetLobby()
{
// It's safe to un-constify the returned lobby if we're being called through a non-const reference ourselves.
return const_cast< CTFGSLobby * >( ((const CTFGCServerSystem *)this)->GetLobby() );
}
void CTFGCServerSystem::DumpLobby()
{
CTFGSLobby *pLobby = GetLobby();
if ( !pLobby )
{
Msg( "Failed to find lobby shared object\n" );
return;
}
pLobby->SpewDebug();
}
bool CTFGCServerSystem::HasLobby() const
{
return GetLobby() != NULL;
}
void CTFGCServerSystem::SetHibernation( bool bHibernating )
{
// !FIXME! Need to get rid of all the hibernation crap. We don't really need it
}
bool CTFGCServerSystem::ShouldHideServer()
{
// !NO! Don't set this right now. We'll just pass the "hidden" tag and so the server
// browser wil not list us.
// if ( m_bMMServerMode && tf_mm_strict.GetBool() )
// return true;
return false;
}
bool CTFGCServerSystem::SteamIDAllowedToConnect(const CSteamID &steamID) const
{
// If we're not in strict mode, anybody can join!
if ( !m_bMMServerMode || tf_mm_strict.GetInt() != 1 )
return true;
// If we don't have a match, nobody can join
const CMatchInfo *pMatchInfo = GetMatch();
if ( !pMatchInfo )
{
return false;
}
const CMatchInfo::PlayerMatchData_t *pMatchData = pMatchInfo->GetMatchDataForPlayer( steamID );
if ( !pMatchData || pMatchData->bDropped )
{
// Not in the match or was dropped, reject
return false;
}
return true;
}
////-----------------------------------------------------------------------------
//int CTFGCServerSystem::GetTeamForLobbyMember( const CSteamID &steamId ) const
//{
// const CTFGSLobby *pLobby = GetLobby();
// if ( !pLobby )
// {
// return DOTA_TEAM_NOTEAM;
// }
//
// int team = pLobby->GetMemberTeam( steamId );
//
// switch ( team )
// {
// case DOTA_GC_TEAM_GOOD_GUYS:
// return DOTA_TEAM_GOODGUYS;
//
// case DOTA_GC_TEAM_BAD_GUYS:
// return DOTA_TEAM_BADGUYS;
//
// case DOTA_GC_TEAM_BROADCASTER:
// case DOTA_GC_TEAM_PLAYER_POOL:
// case DOTA_GC_TEAM_SPECTATOR:
// return TEAM_SPECTATOR;
// }
//
// return DOTA_TEAM_NOTEAM;
//}
//
////-----------------------------------------------------------------------------
//bool CTFGCServerSystem::IsLobbyMemberBroadcaster( const CSteamID &steamId ) const
//{
// const CTFGSLobby *pLobby = GetLobby();
// if ( !pLobby )
// {
// return false;
// }
//
// return pLobby->GetMemberTeam( steamId ) == DOTA_GC_TEAM_BROADCASTER;
//}
//
////-----------------------------------------------------------------------------
//ELanguage CTFGCServerSystem::GetBroadcasterLanguage( const CSteamID &steamId ) const
//{
// const CTFGSLobby *pLobby = GetLobby();
// if ( !pLobby )
// {
// return k_Lang_English;
// }
//
// if ( pLobby->GetMemberTeam( steamId ) != DOTA_GC_TEAM_BROADCASTER )
// return k_Lang_English;
//
// int index = pLobby->GetMemberIndexBySteamID( steamId );
// if ( index < 0 )
// return k_Lang_English;
//
// const CTFLobbyMember* pMember = pLobby->GetMemberDetails( index );
// switch( pMember->slot() )
// {
// default:
// case 1:
// return k_Lang_English;
// case 2:
// return k_Lang_German;
// case 3:
// return k_Lang_Simplified_Chinese;
// case 4:
// return k_Lang_Russian;
// }
//
// return k_Lang_English;
//}
//-----------------------------------------------------------------------------
CON_COMMAND( tf_server_lobby_debug, "Prints server lobby object" )
{
GTFGCClientSystem()->DumpLobby();
}
ConVar dbg_spew_connected_players_level( "dbg_spew_connected_players_level", "0", FCVAR_NONE, "If enabled, server will spew connected player GC updates\n" );
// Inform the GC of any change in the connected players
void CTFGCServerSystem::UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event event, bool bForceSendMessages )
{
VPROF_BUDGET( "CTFGCServerSystem::UpdateConnectedPlayersAndServerInfo", VPROF_BUDGETGROUP_OTHER_NETWORKING );
// Don't bother sending if we aren't initialized yet
if ( gpGlobals->maxClients == 0 || TFGameRules() == NULL )
return;
/// TODO ROLLING MATCH: Remove event field from this message. We might just ignore some events, and they're not
/// useful.
// Don't send heartbeats while we're waiting for reliable messages to process, our state is not in sync with what we
// tried to send to the GC, and sending a new heartbeat before pending messages have been responded to isn't
// helpful.
if ( BPendingReliableMessages() )
{ return; }
// Or if we're in the waiting period to kick off a new match -- if all pending messages came back, our lobby now
// reflects the requested match, but we haven't actually launched it yet, so heartbeats would not be valid.
if ( m_flWaitingForNewMatchTime != 0.f )
{ return; }
const CTFGSLobby *pLobby = GetLobby();
if ( !pLobby || !m_bMMServerMode )
{
Assert( event == CMsgGameServerMatchmakingStatus_Event_None );
}
double now = Plat_FloatTime();
if ( dbg_spew_connected_players_level.GetInt() >= 4 ) { Msg( "UpdateConnectedPlayers ======================================\n" ); }
static ConVarRef sv_visiblemaxplayers( "sv_visiblemaxplayers" );
CProtoBufMsg<CMsgGameServerMatchmakingStatus> msg( k_EMsgGCGameServerMatchmakingStatus );
ServerMatchmakingState eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING;
TF_MatchmakingMode eGameServerInfoMatchmakingMode = TF_Matchmaking_INVALID;
CUtlString sGameServerInfoMap;
CUtlString sGameServerInfoTags;
int nBotCountToSend = -1;
float flSendInterval = 60.0f;
int nUnconnectedPlayerReservationRequests = 0;
bool bLobbyIncorrect = false;
CUtlVector<CSteamID> vecFailedLoaders;
TF_GC_GameState gcState = TF_GC_GAMESTATE_DISCONNECT;
CMatchInfo *pMatch = GetMatch();
const IMatchGroupDescription* pMatchDesc = pMatch ? GetMatchGroupDescription( pMatch->m_eMatchGroup ) : NULL;
// Build list of currently connected clients, and classify them according to their role
struct Reservation_t
{
CSteamID m_steamID;
int m_nEntindex;
bool m_bActive;
};
CUtlVector< Reservation_t > vecReservationRequests;
CUtlVector<CSteamID> vecConnectedPlayers;
int nAdminSlots = 0;
int nAdHocPlayers = 0;
int nMatchPlayers = 0;
int nBots = 0;
for ( int i = 1; i <= gpGlobals->maxClients; i++ )
{
const CSteamID *pPlayerSteamID = engine->GetClientSteamIDByPlayerIndex( i );
// Filter out non-players
player_info_t sPlayerInfo;
bool bActive = false;
if ( engine->GetPlayerInfo( i, &sPlayerInfo ) )
{
if ( sPlayerInfo.ishltv || sPlayerInfo.isreplay )
{
++nAdminSlots;
continue;
}
if ( sPlayerInfo.fakeplayer )
{
++nBots;
continue;
}
if ( pPlayerSteamID == NULL || !pPlayerSteamID->IsValid() )
{
// This can occur in lan-mode
Warning( "Player with no steam ID, counting as ad-hoc\n" );
}
bActive = true;
}
else
{
// Client not "active", but might be connected.
// this happens during changelevel
if ( pPlayerSteamID == NULL || !pPlayerSteamID->IsValid() )
{
continue;
}
// Connected, but not active.
bActive = false;
// Shove in a dummy name or debug spew
V_strcpy_safe( sPlayerInfo.name, pPlayerSteamID->Render() );
}
// Some kind of player, add them to match players or ad-hoc
CSteamID playerSteamID;
if ( pPlayerSteamID && pPlayerSteamID->IsValid() )
playerSteamID = *pPlayerSteamID;
CMatchInfo::PlayerMatchData_t *pMatchPlayer = ( pMatch && playerSteamID.IsValid() ) \
? pMatch->GetMatchDataForPlayer( playerSteamID ) \
: NULL;
bool bMatchPlayer = pMatchPlayer && !pMatchPlayer->bDropped;
if ( bMatchPlayer )
{ ++nMatchPlayers; }
else
{ ++nAdHocPlayers; }
if ( dbg_spew_connected_players_level.GetInt() >= 4 ) { Msg( " Client[%d]: %s '%s':\n", i, playerSteamID.Render(), sPlayerInfo.name ); }
//
// !! In lan mode, this player may not have a steamID. They can't be a lobby member or similar, so the below
// !! code should just assume they're ad-hoc if !playerSteamID.IsValid()
//
if ( playerSteamID.IsValid() )
vecConnectedPlayers.AddToTail( playerSteamID );
// If we don't have a lobby, then we may still be running a match after a GC crash/reboot, in which case the
// lobby might've been lost -- but we're still expected to complete the match on our own authority and report
// the result.
/// XXX(JohnS): Ideally, in the state where the GC rebooted and the lobby disintegrated, we'd have some way
/// to tell the GC to recreate the lobby on its end when we re-establish, rather than finishing
/// out a phantom match -- it doesn't know the user is still in a match until the match result
/// arrives. However, as we locally track and report the match result and any abandons, the user
/// can't really exploit this state other than potentially alt-F4ing and requeuing faster than
/// their abandon timeout. The GC, however, loses the ability to kick the player from this
/// lobby. (that it no longer knows about)
if ( pLobby )
{
// If he's in the lobby, them count him as a connected player.
// Otherwise, he's an ad-hoc join.
CMsgGameServerMatchmakingStatus_PlayerConnectState sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_INVALID;
const CTFLobbyMember *pMember = pLobby->GetMemberDetails( playerSteamID );
if ( pMember )
{
CTFLobbyMember_ConnectState eLobbyState = pMember->connect_state();
if ( dbg_spew_connected_players_level.GetInt() >= 4 )
{
Msg( " '%s' In lobby with state %s\n", sPlayerInfo.name,
CTFLobbyMember_ConnectState_Name( eLobbyState ).c_str() );
}
switch ( eLobbyState )
{
case CTFLobbyMember_ConnectState_RESERVATION_PENDING:
// Check if we have match data for this guy
if ( !bMatchPlayer )
{
bLobbyIncorrect = true;
vecReservationRequests.AddToTail( { *pPlayerSteamID, i, bActive } );
}
break;
case CTFLobbyMember_ConnectState_RESERVED:
// Only count them as actually "connected" if they are active.
// We do not count them as "connected", to make sure we treat a
// disconnection before they become "active" as a failure to load,
// but a disconnection after they become active as a "leaver"
if ( bActive )
{
sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_CONNECTED;
bLobbyIncorrect = true;
}
else
{
sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_RESERVED;
if ( eLobbyState != CTFLobbyMember_ConnectState_RESERVED )
bLobbyIncorrect = true;
}
break;
case CTFLobbyMember_ConnectState_CONNECTED:
sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_CONNECTED;
break;
case CTFLobbyMember_ConnectState_DISCONNECTED:
sendPlayerConnectState = CMsgGameServerMatchmakingStatus_PlayerConnectState_CONNECTED;
bLobbyIncorrect = true;
break;
default:
AssertMsg1( false, "Unknown lobby member state %d", eLobbyState );
break;
}
}
else if ( m_pMatchInfo && !m_pMatchInfo->m_bMatchEnded )
{
// Competitive match, player missing from lobby
if ( bMatchPlayer )
{
// Player was part of the match, but GC removed them.
MMLog( "Removing match player %s -- dropped from lobby, but still in match and game\n",
playerSteamID.Render() );
EjectMatchPlayer( playerSteamID, TFMatchLeaveReason_GC_REMOVED );
nMatchPlayers--;
}
else if ( tf_mm_strict.GetInt() == 1 )
{
// A player is present that shouldn't be
MMLog( "!! Unknown player in managed match %s\n", playerSteamID.Render() );
KickRemovedMatchPlayer( playerSteamID );
nAdHocPlayers--;
}
}
else
{
// Not a managed match
if ( dbg_spew_connected_players_level.GetInt() >= 4 )
{
Msg( " '%s' Not in lobby, client is ad-hoc join\n", sPlayerInfo.name );
}
}
if ( sendPlayerConnectState != CMsgGameServerMatchmakingStatus_PlayerConnectState_INVALID )
{
CMsgGameServerMatchmakingStatus_Player *pMsgPlayer = msg.Body().add_players();
pMsgPlayer->set_steam_id( playerSteamID.ConvertToUint64() );
pMsgPlayer->set_connect_state( sendPlayerConnectState );
}
}
} // end For each client
//
// Now, check match for players that we are tracking but are not connected, and count them in the total and the
// status message
//
if ( pMatch && !pMatch->BMatchTerminated() )
{
int nTotalMatch = pMatch->GetNumTotalMatchPlayers();
for ( int idx = 0; idx < nTotalMatch; idx++ )
{
CMatchInfo::PlayerMatchData_t *pPlayer = pMatch->GetMatchDataForPlayer( idx );
// Don't care if they are now dropped or were handled in the connected players loop above
if ( pPlayer->bDropped || vecConnectedPlayers.Find( pPlayer->steamID ) != vecConnectedPlayers.InvalidIndex() )
{ continue; }
if ( pPlayer->bConnected )
{
MMLog( "!! Match player %s not present but marked connected\n", pPlayer->steamID.Render() );
}
// Note that if the GC lost our lobby (which should only occur due to system failure on the other end), we
// just keep dutifully sending status updates for the players we have as long as we have a match
if ( pLobby && !pLobby->GetMemberDetails( pPlayer->steamID ) )
{
// Player was part of the match, but GC removed them.
MMLog( "Removing player %s, not present in match and dropped from lobby\n",
pPlayer->steamID.Render() );
SetMatchPlayerDropped( pPlayer->steamID, TFMatchLeaveReason_GC_REMOVED );
}
else
{
// We are holding a valid reservation. Add this fact to the message, to confirm
// that we are aware of the player.
nMatchPlayers++;
CMsgGameServerMatchmakingStatus_Player *pMsgPlayer = msg.Body().add_players();
pMsgPlayer->set_steam_id( pPlayer->steamID.ConvertToUint64() );
if ( dbg_spew_connected_players_level.GetInt() >= 4 )
{ Msg( " Player[%d]: %s reserved\n", msg.Body().players_size(), pPlayer->steamID.Render() ); }
pMsgPlayer->set_connect_state( CMsgGameServerMatchmakingStatus_PlayerConnectState_RESERVED );
bLobbyIncorrect = true;
}
}
}
//
// Scan lobby, and check for lobby player entries that don't match our local state.
//
if ( pLobby )
{
if ( dbg_spew_connected_players_level.GetInt() >= 4 )
{ Msg( "Checking all connected players are marked connected in lobby:\n" ); }
for ( int i = 0; i < pLobby->GetNumMembers(); i++ )
{
const CTFLobbyMember *pMemberDetails = pLobby->GetMemberDetails( i );
Assert( pMemberDetails );
if ( !pMemberDetails )
continue;
CSteamID steamID( pMemberDetails->id() );
CTFLobbyMember_ConnectState eLobbyState = pLobby->GetMemberConnectState( i );
if ( dbg_spew_connected_players_level.GetInt() >= 4 )
{ Msg( " Lobby member %s is in state %s\n", steamID.Render(), CTFLobbyMember_ConnectState_Name( eLobbyState ).c_str() ); }
int iConnectedPlayer = vecConnectedPlayers.Find( steamID );
if ( iConnectedPlayer >= 0 )
{ continue; } // we handled them earlier
// Player is not currently connected. Check against what the lobby thinks
switch ( eLobbyState )
{
case CTFLobbyMember_ConnectState_RESERVATION_PENDING:
{
// Check if we already have a reservation for this guy
CMatchInfo::PlayerMatchData_t *pMatchPlayer = GetMatch() ? GetMatch()->GetMatchDataForPlayer( steamID ) : NULL;
if ( GetMatch() && ( !pMatchPlayer || pMatchPlayer->bDropped ) )
{
bLobbyIncorrect = true;
vecReservationRequests.AddToTail( { steamID, 0, false } );
++nUnconnectedPlayerReservationRequests;
}
else
{
if ( dbg_spew_connected_players_level.GetInt() >= 4 )
{ Msg( " Player[%d]: %s requested reservation. We already had one.\n", msg.Body().players_size(), steamID.Render() ); }
}
} break;
case CTFLobbyMember_ConnectState_RESERVED:
// We'll handle it below when we process our reservations
break;
case CTFLobbyMember_ConnectState_CONNECTED:
if ( dbg_spew_connected_players_level.GetInt() >= 4 )
{ Msg( " Lobby member %s no longer connected, lobby is incorrect\n", steamID.Render() ); }
bLobbyIncorrect = true;
break;
case CTFLobbyMember_ConnectState_DISCONNECTED:
break;
default:
AssertMsg1( false, "Unknown lobby member state %d", eLobbyState );
break;
}
}
}
// Now we've scanned connected players, our match, and the lobby object. Count up the total taken slots, and how
// many slots the match could have (0 if no match) These are slots that are spoken for, not necessarily currently
// connected
// NOTE: These might be updated by accepting reservations or dropping players in the next section
bool bLiveMatch = pMatch && pMatchDesc && !pMatch->m_bMatchEnded;
// TODO ROLLING MATCHES: Need a check for no-latejoins state for after we've sent a match result?
int nMaxMatchPlayers = bLiveMatch ? pMatch->GetCanonicalMatchSize() : 0;
int nMaxHumans = gpGlobals->maxClients - nAdminSlots;
{
// Maybe cap visible max humans. Honor the override MvM mode might apply, but if we are accepting arbitrary new
// matches expose the real value we would allow a new potentially non-mvm match to use.
int nLimitVisibleSlots = sv_visiblemaxplayers.GetInt();
if ( m_bOverridingVisibleMaxPlayers && !bLiveMatch && m_bMMServerMode )
{ nLimitVisibleSlots = m_iSavedVisibleMaxPlayers; }
// Don't limit visible slots to below the current match
if ( nMaxMatchPlayers > 0 )
{ nLimitVisibleSlots = Max( nMaxMatchPlayers, nLimitVisibleSlots ); }
if ( nLimitVisibleSlots > 0 )
{ nMaxHumans = Min( nMaxHumans, nLimitVisibleSlots ); }
}
int nHumans = nAdHocPlayers + nMatchPlayers;
int nClients = nHumans + nBots + nAdminSlots;
// Maximum nHumans should be allowed to be. Max clients - AdminSlots, capped to visiblemaxplayers
// If we've never added a player to our match this is the first think
bool bNewMatch = bLiveMatch && pMatch->GetNumTotalMatchPlayers() == 0;
// If our current state allows us to accept new match players
bool bRequestMatchLateJoin = bLiveMatch && \
nHumans < nMaxMatchPlayers && \
nClients < gpGlobals->maxClients && \
pMatchDesc->ShouldRequestLateJoin();
//
// Check if the GC is requesting us to make some more reservations, and accepting them would not exceed
// desired match size or engine capabilities.
//
if ( pLobby && vecReservationRequests.Count() &&
( bNewMatch || bRequestMatchLateJoin ) &&
nUnconnectedPlayerReservationRequests + nHumans <= nMaxMatchPlayers &&
nUnconnectedPlayerReservationRequests + nClients <= gpGlobals->maxClients )
{
MMLog( "GC is requesting us to reserve %d slots.\n", vecReservationRequests.Count() );
// Accept one at a time and check if we can handle more
FOR_EACH_VEC( vecReservationRequests, idx )
{
const CTFLobbyMember *pMember = pLobby->GetMemberDetails( vecReservationRequests[ idx ].m_steamID );
AcceptGCReservation( vecReservationRequests[ idx ].m_steamID, pMember, !bNewMatch,
vecReservationRequests[ idx ].m_nEntindex, vecReservationRequests[ idx ].m_bActive );
// Add them to our message for this pass
CMsgGameServerMatchmakingStatus_Player *pMsgPlayer = msg.Body().add_players();
pMsgPlayer->set_steam_id( vecReservationRequests[ idx ].m_steamID.ConvertToUint64() );
pMsgPlayer->set_connect_state( CMsgGameServerMatchmakingStatus_PlayerConnectState_RESERVED );
}
// We promised more people slots, recompute this
nMatchPlayers += nUnconnectedPlayerReservationRequests;
nHumans += nUnconnectedPlayerReservationRequests;
nClients += nUnconnectedPlayerReservationRequests;
bRequestMatchLateJoin = bRequestMatchLateJoin && \
nHumans < nMaxMatchPlayers && \
nClients < gpGlobals->maxClients && \
pMatchDesc && pMatchDesc->ShouldRequestLateJoin();
}
else if ( nUnconnectedPlayerReservationRequests )
{
MMLog( "Refused %d reservations -- not accepting match players or exceeds capacity\n",
vecReservationRequests.Count() );
}
// Check if they think that they are acknowledging some players, make sure
// we would have decided to send a message anyway, even without their event
if ( event == CMsgGameServerMatchmakingStatus_Event_AcknowledgePlayers )
{
Assert( bLobbyIncorrect == true );
}
//
// Clean up complete match if all players have left and the GC has dissolved the lobby.
//
// Deleting this should clear us up to accept new matches below,
// where our ready-for-match state depends on !pLobby && !pMatch.
//
// Don't clean up if GC hasn't acknowledged dissolution of lobby yet, or we'll have a lobby with no associated
// match to indicate what state it was in. If the GC is MIA to clean-up lobbies that's okay, we can't start a
// new match until it's ready anyway, and the empty-with-lobby below check will kill us if we get stuck in this
// state.
if ( vecConnectedPlayers.Count() == 0 &&
m_pMatchInfo && !pLobby && m_pMatchInfo->m_bMatchEnded )
{
MMLog( "Cleaning out finished match %llu\n", m_pMatchInfo->m_nMatchID );
delete m_pMatchInfo;
m_pMatchInfo = NULL;
bLiveMatch = false;
pMatch = NULL;
pMatchDesc = NULL;
}
// Check if we're empty with a lobby. Ordinarily, we shouldn't linger too long in this state. Either we're in
// the process of timing out everyone as abandoners (which should take a lot less than this timeout) or the GC
// is down. But if that state persists for two hours, assume we're in a bad stuck state and reboot.
if ( pLobby && vecConnectedPlayers.Count() == 0 )
{
if ( m_flTimeBecameEmptyWithLobby == 0.0 )
{
m_flTimeBecameEmptyWithLobby = now;
}
else
{
int nSecondsEmptyWithLobby = int( now - m_flTimeBecameEmptyWithLobby );
int nTimeoutMinutes = ( BPendingReliableMessages() || m_pMatchInfo ) ? k_InvalidState_Timeout_With_Match \
: k_InvalidState_Timeout_Without_Match;
if ( nSecondsEmptyWithLobby > nTimeoutMinutes*60 )
{
MMLog( "**** Server has been empty with a lobby for %d seconds. Quitting\n", nSecondsEmptyWithLobby );
AbortInvalidMatchState();
}
}
}
else
{
m_flTimeBecameEmptyWithLobby = 0.0;
}
// Determine game state
gcState = TF_GC_GAMESTATE_GAME_IN_PROGRESS;
switch ( TFGameRules()->State_Get() )
{
case GR_STATE_INIT:
gcState = TF_GC_GAMESTATE_STATE_INIT;
break;
case GR_STATE_PREGAME:
case GR_STATE_STARTGAME:
case GR_STATE_PREROUND:
case GR_STATE_RESTART:
gcState = TF_GC_GAMESTATE_STRATEGY_TIME;
break;
default:
Assert( false );
case GR_STATE_RND_RUNNING:
case GR_STATE_BETWEEN_RNDS:
case GR_STATE_BONUS:
break;
case GR_STATE_TEAM_WIN:
case GR_STATE_STALEMATE:
if ( TFGameRules()->IsMannVsMachineMode() )
{
// *Currently* can only end in victory (or dissolves because everyone leaves)
if (
TFGameRules()->State_Get() == GR_STATE_TEAM_WIN
&& TFGameRules()->GetWinningTeam() == TF_TEAM_PVE_DEFENDERS )
{
gcState = TF_GC_GAMESTATE_POST_GAME;
}
}
else if ( TFGameRules()->IsCompetitiveMode() )
{
if ( TFGameRules()->State_Get() == GR_STATE_GAME_OVER )
{
gcState = TF_GC_GAMESTATE_POST_GAME;
}
}
break;
case GR_STATE_GAME_OVER:
gcState = TF_GC_GAMESTATE_GAME_IN_PROGRESS;
if ( TFGameRules()->IsMannVsMachineMode() ||
TFGameRules()->IsCompetitiveMode() ) // right?
{
gcState = TF_GC_GAMESTATE_DISCONNECT;
}
break;
}
// What state are we?
if ( m_bMMServerMode )
{
static ConVarRef sv_tags( "sv_tags" );
eGameServerInfoMatchmakingMode = TF_Matchmaking_LADDER;
nBotCountToSend = -1;
sGameServerInfoMap = STRING( gpGlobals->mapname );
sGameServerInfoTags = sv_tags.GetString();
sGameServerInfoTags.Clear();
// Set the "map" to the current challenge, if in MvM
if ( TFGameRules()->IsMannVsMachineMode() )
{
const char *pszFilenameShort = g_pPopulationManager ? g_pPopulationManager->GetPopulationFilenameShort() : NULL;
if ( pszFilenameShort && pszFilenameShort[0] )
{
sGameServerInfoMap = pszFilenameShort;
}
}
// Determine state
if ( !m_pMatchInfo && !pLobby )
{
// No match, lobby, or players, ready for match
if ( BPendingReliableMessages() )
{
eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING;
if ( m_eLastGameServerUpdateState != eGameServerInfoState )
{ MMLog( "No match, but have not finished sending reliable messages, not re-enrolling in MM yet\n" ); }
}
else
{
eGameServerInfoState = ServerMatchmakingState_EMPTY;
if ( m_eLastGameServerUpdateState != eGameServerInfoState )
{ MMLog( "No match, but configured for MM, enrolling in matchmaking\n" ); }
}
// Unless we're not setup with no actual usable slots or have random unknown humans in strict mode
if ( nClients >= gpGlobals->maxClients || nMaxHumans < 1 ||
( nHumans && tf_mm_strict.GetInt() == 1 ) )
{
eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING;
if ( m_eLastGameServerUpdateState != eGameServerInfoState )
{
MMLog( "!! No match, but no usable slots or unexpected clients, cannot enroll in matchmaking. "
"[ nClients %d, maxClients %d, nHumans %d, nMaxHumans %d ]\n",
nClients, gpGlobals->maxClients, nHumans, nMaxHumans );
}
}
}
else if ( bLiveMatch )
{
// Have a running match.
eGameServerInfoState = bRequestMatchLateJoin ? ServerMatchmakingState_ACTIVE_MATCH_REQUESTING_LATE_JOIN \
: ServerMatchmakingState_ACTIVE_MATCH;
}
else
{
// We have a match but it isn't live, or we have no match but the GC hasn't torn down the lobby yet ( we
// should have either rejected the lobby in SOCreated or sent a cleanup message when ending the match, but
// our GC connection may be lagged, just stay out of the pool until we reconcile )
if ( m_eLastGameServerUpdateState != eGameServerInfoState )
{ MMLog( "Match state is not in sync with GC, remaining out of MM until lobby is cleaned up\n" ); }
eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING;
}
}
// This is probably not worth the risk / reward right now. We've given instructions
// telling server operators how to avoid this from happening, and it might break something
// // Check if we have a lobby, and they have switched to/from MvM mode, then don't
// // put us in matchmaking for now
// bool bMapIsMvmMap = ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() );
// if ( ( pLobby != NULL ) && ( bMapIsMvmMap != bIsMvmMode ) )
// {
// eGameServerInfoMatchmakingMode = TF_Matchmaking_INVALID;
// eGameServerInfoState = ServerMatchmakingState_NOT_PARTICIPATING;
// MMLog( "Sending NOT_PARTICIPATING. Is MvM Map: %d, tf_mm_servermode=%d\n", bMapIsMvmMap ? 1 : 0, tf_mm_servermode.GetInt() );
// }
int nSlotsFree = nMaxHumans - nHumans;
// Check if number of slots available is changing. Our urgency to notify the GC about this
// change depends on which direction it is changing!
if ( nSlotsFree < m_nLastGameServerUpdateSlotsFree )
{
// We currently have fewer slots available than the GC thinks we do.
// This is an important state change and we need to let the GC know about
// this immediately, otherwise it might ask us to fill reservations we cannot
// satisfy. We want the window for this race condition to be as small as
// possible.
bForceSendMessages = true;
}
else if ( nSlotsFree > m_nLastGameServerUpdateSlotsFree )
{
// We have more slots open than the GC thinks we do. We should let the GC
// know relatively soon, but it's really not urgent that we flush this out
// *immediately*. Also, because players come and go frequently (especially
// in PvP), having this timer avoids massive spam if tons of players all decide
// to leave at once.
flSendInterval = Min( flSendInterval, 10.0f );
}
// Check if we MUST send a message, no matter how recently we sent the last update.
if ( event == CMsgGameServerMatchmakingStatus_Event_None &&
!bForceSendMessages &&
( eGameServerInfoState == m_eLastGameServerUpdateState ) &&
( eGameServerInfoMatchmakingMode == m_eLastGameServerUpdateMatchmakingMode ) &&
// map changes are infrequent, and matter quite a bit, so always send them
Q_stricmp( m_sLastGameServerUpdateMap, sGameServerInfoMap ) == 0 )
{
// No need to send periodic updates if we're not participating and don't think we have a lobby or match at all.
if ( eGameServerInfoState == ServerMatchmakingState_NOT_PARTICIPATING && !pLobby && !m_pMatchInfo )
return;
// Check for certain rules changes. When they change, we care about them being
// eventually correct, but it's not urgent
if ( ( Q_stricmp( m_sLastGameServerUpdateTags, sGameServerInfoTags ) != 0 ) ||
( nMaxHumans != m_nLastGameServerUpdateMaxHumans ) ||
( nBotCountToSend != m_nLastGameServerUpdateBotCount ) )
{
flSendInterval = Min( flSendInterval, 20.0f );
}
// If lobby is incorrect in an ordinary way (player left, etc),
// flush the change decently quickly
if ( pLobby && bLobbyIncorrect )
{
// Send updates more quickly if the GC hasn't acknowledged, but don't DDoS. Ideally the event that made the
// lobby incorrect triggered a Update( bForce = true );
flSendInterval = Min( flSendInterval, 10.0f );
}
if ( now < m_timeLastSendGameServerInfoAndConnectedPlayers + flSendInterval )
{ return; }
}
// Fill in info about our connection state
msg.Body().set_server_version( engine->GetServerVersion() );
msg.Body().set_matchmaking_state( eGameServerInfoState );
if ( eGameServerInfoState == ServerMatchmakingState_NOT_PARTICIPATING )
{
msg.Body().set_match_group( k_nMatchGroup_Invalid );
if ( dbg_spew_connected_players_level.GetInt() >= 2 )
{
MMLog("Sending CMsgGameServerMatchmakingStatus (state=%s)\n",
ServerMatchmakingState_Name( msg.Body().matchmaking_state() ).c_str() );
}
}
else
{
static ConVarRef sv_region( "sv_region" );
msg.Body().set_server_region( sv_region.GetInt() );
msg.Body().set_server_loadavg( GetCPUUsage() );
msg.Body().set_server_dedicated( engine->IsDedicatedServer() );
msg.Body().set_server_trusted( tf_mm_trusted.GetBool() );
msg.Body().set_matchmaking_mode( eGameServerInfoMatchmakingMode );
msg.Body().set_map( sGameServerInfoMap );
msg.Body().set_game_state( gcState );
if ( pLobby )
msg.Body().set_lobby_mm_version( pLobby->GetLobbyMMVersion() );
if ( nBotCountToSend >= 0 )
msg.Body().set_bot_count( (uint32)nBotCountToSend );
Assert( nMaxHumans > 0 );
msg.Body().set_max_players( nMaxHumans );
Assert( nSlotsFree >= 0 );
msg.Body().set_slots_free( nSlotsFree );
msg.Body().set_tags( sGameServerInfoTags );
msg.Body().set_strict( tf_mm_strict.GetInt() );
if ( event != CMsgGameServerMatchmakingStatus_Event_None )
{ msg.Body().set_event( event ); }
if ( ( dbg_spew_connected_players_level.GetInt() >= 2 ) ||
( event != CMsgGameServerMatchmakingStatus_Event_None && dbg_spew_connected_players_level.GetInt() >= 1 ) )
{
MMLog("Sending CMsgGameServerMatchmakingStatus (state=%s, slots_free=%d, event=%s, %s)\n",
ServerMatchmakingState_Name( msg.Body().matchmaking_state() ).c_str(),
msg.Body().slots_free(),
CMsgGameServerMatchmakingStatus_Event_Name( msg.Body().event() ).c_str(),
( tf_mm_trusted.GetBool() ? ", trusted=true" : "" )
);
}
if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() )
{
msg.Body().set_mvm_credits_acquired( MannVsMachineStats_GetAcquiredCredits( -1 ) );
msg.Body().set_mvm_credits_dropped( MannVsMachineStats_GetAcquiredCredits( -1 ) );
msg.Body().set_mvm_wave( MannVsMachineStats_GetCurrentWave() );
}
EMatchGroup eCurrentGroup = k_nMatchGroup_Invalid;
if ( m_pMatchInfo )
{
eCurrentGroup = m_pMatchInfo->m_eMatchGroup;
}
msg.Body().set_match_group( eCurrentGroup );
}
// Check if we MUST send a message, no matter how recently we sent the last update.
if ( event == CMsgGameServerMatchmakingStatus_Event_None &&
!bForceSendMessages &&
( msg.Body().lobby_mm_version() == m_nLastGameServerUpdateLobbyMMVersion ) &&
( msg.Body().matchmaking_state() == m_eLastGameServerUpdateState ) &&
( msg.Body().matchmaking_mode() == m_eLastGameServerUpdateMatchmakingMode ) &&
// map changes are infrequent, and matter quite a bit, so always send them
Q_stricmp( m_sLastGameServerUpdateMap, msg.Body().map().c_str() ) == 0 )
{
// No need to send periodic updates if we're not participating and don't think we have a lobby or match at all.
if ( msg.Body().matchmaking_state() == ServerMatchmakingState_NOT_PARTICIPATING && !pLobby && !m_pMatchInfo )
return;
// Check for certain rules changes. When they change, we care about them being
// eventually correct, but it's not urgent
if ( ( Q_stricmp( m_sLastGameServerUpdateTags, msg.Body().tags().c_str() ) != 0 ) ||
( msg.Body().max_players() != (uint32)m_nLastGameServerUpdateMaxHumans ) ||
( msg.Body().bot_count() != (uint32)m_nLastGameServerUpdateBotCount ) )
{
flSendInterval = Min( flSendInterval, 20.0f );
}
// If lobby is incorrect in an ordinary way (player left, etc),
// flush the change decently quickly
if ( pLobby && bLobbyIncorrect )
{
// Send updates more quickly if the GC hasn't acknowledged, but don't DDoS. Ideally the event that made the
// lobby incorrect triggered a Update( bForce = true );
flSendInterval = Min( flSendInterval, 10.0f );
}
if ( now < m_timeLastSendGameServerInfoAndConnectedPlayers + flSendInterval )
{ return; }
}
GCClientSystem()->BSendMessage( msg );
// Remember what/when we sent, so we can tell next time if we need to send
m_timeLastSendGameServerInfoAndConnectedPlayers = now;
m_eLastGameServerUpdateMatchmakingMode = msg.Body().matchmaking_mode();
m_eLastGameServerUpdateState = msg.Body().matchmaking_state();
m_sLastGameServerUpdateMap = msg.Body().map().c_str();
m_sLastGameServerUpdateTags = msg.Body().tags().c_str();
m_nLastGameServerUpdateBotCount = nBotCountToSend;
m_nLastGameServerUpdateMaxHumans = nMaxHumans;
m_nLastGameServerUpdateSlotsFree = nSlotsFree;
m_nLastGameServerUpdateLobbyMMVersion = msg.Body().lobby_mm_version();
// Remember when we started requesting late join, so we can compare it to our lobby's late-join state to reason
// about how long we've been waiting.
if ( eGameServerInfoState == ServerMatchmakingState_ACTIVE_MATCH_REQUESTING_LATE_JOIN )
{
if ( m_flTimeRequestedLateJoin == -1.f )
{
m_flTimeRequestedLateJoin = CRTime::RTime32TimeCur();
MMLog( "Requested late join for active match\n" );
}
}
else if ( m_flTimeRequestedLateJoin != -1.f )
{
MMLog( "Stopped requesting late join for active match after %.02fs\n",
CRTime::RTime32TimeCur() - m_flTimeRequestedLateJoin );
m_flTimeRequestedLateJoin = -1.f;
}
// Only late join eligible when are requesting late join, we have a lobby from the GC, and it has marked itself as
// late join eligible. If we've lost our lobby or it hasn't updated to become eligible, there may be GC connection
// difficulties.
// We only update this at the end of updates, rather than on the fly, to ensure we don't expose this value prior to
// processing other updates in the lobby object. For instance, the lobby might remove us from late join and give us
// reserved members at the same time, we don't want callers to see one, but not the other.
m_bLateJoinEligible = m_flTimeRequestedLateJoin != -1.f && GetLobby() && GetLobby()->GetLateJoinEligible();
}
// ***************************************************************************************************************
void CTFGCServerSystem::SendMvMVictoryResult()
{
// Note that we don't have to have an *ended* match -- MvM code technically allows players to continue in the same
// match and achieve multiple victories.
Assert( m_pMatchInfo );
CTFGSLobby *pLobby = GetLobby();
if ( !pLobby )
{
// FIXME - We should be able to submit this even if the GC reboots and loses our lobby state (though it wont
// happen that often, as the GC tries to revive lobby state from memcached)
MMLog( "CTFGCServerSystem::MvMVictory() -- no lobby, so not sending results to GC\n" );
return;
}
if ( IsMannUpGroup( pLobby->GetMatchGroup() ) )
{
m_mvmVictoryInfo.Init( pLobby );
ReliableMsgMvMVictory *pReliable = new ReliableMsgMvMVictory;
auto &msg = pReliable->Msg().Body();
msg.set_mission_name( m_mvmVictoryInfo.m_sChallengeName );
#ifdef USE_MVM_TOUR
if ( !m_mvmVictoryInfo.m_sMannUpTourOfDuty.IsEmpty() )
{
msg.set_tour_name_mannup( m_mvmVictoryInfo.m_sMannUpTourOfDuty );
}
#endif // USE_MVM_TOUR
msg.set_lobby_id( m_mvmVictoryInfo.m_nLobbyId );
msg.set_event_time( m_mvmVictoryInfo.m_tEventTime );
FOR_EACH_VEC( m_mvmVictoryInfo.m_vPlayerIds, iMember )
{
CMsgMvMVictory_Player *pMsgPlayer = msg.add_players();
pMsgPlayer->set_steam_id( m_mvmVictoryInfo.m_vPlayerIds[ iMember ]);
pMsgPlayer->set_squad_surplus( m_mvmVictoryInfo.m_vSquadSurplus[ iMember ] );
}
pReliable->Enqueue();
}
}
////-----------------------------------------------------------------------------
//// Purpose: Job for being told when the server GC connection is established
////-----------------------------------------------------------------------------
//class CGCClientJobServerWelcome : public GCSDK::CGCClientJob
//{
//public:
// CGCClientJobServerWelcome( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
//
// virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
// {
// CProtoBufMsg<CMsgServerWelcome> msg( pNetPacket );
//
// g_bServerReceivedGCWelcome = true;
//
// GTFGCClientSystem()->UpdateGCServerInfo();
//
// // Validate version
// int engineServerVersion = engine->GetServerVersion();
// g_gcServerVersion = (int)msg.Body().version();
//
// // Version checking is enforced if both sides do not report zero as their version
// if ( engineServerVersion && g_gcServerVersion && engineServerVersion != g_gcServerVersion )
// {
// // If we're out of date exit
// Msg("Version out of date (GC wants %d, we are %d)!\n", g_gcServerVersion, engine->GetServerVersion() );
//
// // If we hibernating, quit now, otherwise we will quit on hibernation
// if ( g_ServerGameDLL.m_bIsHibernating )
// {
// engine->ServerCommand( "quit\n" );
// }
// }
// else
// {
// Msg("GC Connection established for server version %d\n", engine->GetServerVersion() );
// }
//
// return true;
// }
//};
//GC_REG_JOB( GCSDK::CGCClient, CGCClientJobServerWelcome, "CGCClientJobServerWelcome", k_EMsgGCServerWelcome, k_EServerTypeGCClient );
//// temp for tracking down machines submitted stats
//#if defined ( _WIN32 )
//#define WIN32_LEAN_AND_MEAN
//#undef INVALID_HANDLE_VALUE
//#undef DECLARE_HANDLE
//#include <windows.h>
//bool DOTA_GetComputerName( char *pszComputerName, DWORD *length )
//{
// return !!GetComputerName( pszComputerName, length );
//}
//#endif
// **************************************************************************************************
void CTFGCServerSystem::SendRejectLobby()
{
MMLog( "Sending CMsgGameServerKickingLobby to reject stale lobby\n" );
ReliableMsgGameServerKickingLobby *pReliable = new ReliableMsgGameServerKickingLobby();
auto &msg = pReliable->Msg().Body();
msg.set_create_party( false );
if ( GetLobby() )
{
msg.set_lobby_id( GetLobby()->GetGroupID() );
msg.set_lobby_id( GetLobby()->GetMatchID() );
}
pReliable->Enqueue();
}
// **************************************************************************************************
void CTFGCServerSystem::EndManagedMatch( bool bKickPlayersToParties )
{
CMatchInfo *pMatch = GetMatch();
// Sanity
AssertMsg( !pMatch || !pMatch->m_bMatchEnded, "Ending an already ended match" );
if ( !pMatch )
{ return; }
pMatch->SetEnded();
// Cancel launching the new match. Leave the rest of the state alone, we'll send a NewMatch -> EndMatch and things
// will just work out as responses come in.
m_flWaitingForNewMatchTime = 0.f;
if ( !m_pMatchInfo->m_bSentResult )
{
Warning( "Ending a managed match without sending a result" );
Assert( false );
}
ReliableMsgGameServerKickingLobby *pReliable = new ReliableMsgGameServerKickingLobby();
auto &msg = pReliable->Msg().Body();
if ( bKickPlayersToParties )
{
CUtlVector<CSteamID> vecConnectedPlayers;
int total = pMatch->GetNumTotalMatchPlayers();
for ( int idx = 0; idx < total; idx++ )
{
CMatchInfo::PlayerMatchData_t *pMatchPlayer = pMatch->GetMatchDataForPlayer( idx );
if ( !pMatchPlayer->bDropped && pMatchPlayer->bConnected )
{
msg.add_connected_players( pMatchPlayer->steamID.ConvertToUint64() );
}
}
if ( msg.connected_players_size() <= 0 )
{
bKickPlayersToParties = false;
}
}
if ( bKickPlayersToParties )
{
MMLog( "Sending CMsgGameServerKickingLobby, requesting party with %d connected players\n", msg.connected_players_size() );
}
else
{
MMLog( "Sending CMsgGameServerKickingLobby, not requesting party\n" );
}
msg.set_create_party( bKickPlayersToParties );
msg.set_lobby_id( pMatch->m_nLobbyID );
msg.set_match_id( pMatch->m_nMatchID );
pReliable->Enqueue();
}
// **************************************************************************************************
void CTFGCServerSystem::SendPlayerLeftMatch( CSteamID targetPlayer, TFMatchLeaveReason eReason, bool bIsAbandon )
{
CMatchInfo *pMatch = GetMatch();
// Sanity
AssertMsg( pMatch && !pMatch->m_bMatchEnded, "Don't expect to be sending this without a live match" );
if ( !pMatch )
{ return; }
ReliableMsgPlayerLeftMatch *pReliable = new ReliableMsgPlayerLeftMatch();
auto &msg = pReliable->Msg().Body();
msg.set_steam_id( targetPlayer.ConvertToUint64() );
msg.set_leave_reason( eReason );
MMLog( "Sending CMsgPlayerLeftMatch with target of %s [ abandon = %d ]\n", targetPlayer.Render(), bIsAbandon );
msg.set_lobby_id( pMatch->m_nLobbyID );
msg.set_match_id( pMatch->m_nMatchID );
msg.set_was_abandon( bIsAbandon );
pReliable->Enqueue();
}
// **************************************************************************************************
void CTFGCServerSystem::SendCompetitiveMatchResult( GCSDK::CProtoBufMsg< CMsgGC_Match_Result > *pMatchResultMsg )
{
// We should have matchinfo when completing a ladder match
if ( !m_pMatchInfo )
{
Warning( "Sending competitive match results without match info!\n" );
Assert( false );
}
if ( m_pMatchInfo->m_bSentResult )
{
Warning( "Sending competitive match results without an ended match\n" );
Assert( false );
}
ReliableMsgMatchResult *pReliable = new ReliableMsgMatchResult;
auto &msg = pReliable->Msg().Body();
/// XXX(JohnS): With refactor this is now kinda silly. Callers should really just be giving us a CMsgGC_Match_Result
/// instead of the wrapper.
msg.CopyFrom( pMatchResultMsg->Body() );
pReliable->Enqueue();
m_pMatchInfo->m_bSentResult = true;
}
// **************************************************************************************************
bool CTFGCServerSystem::BLateJoinEligible()
{
return m_bLateJoinEligible;
}
// **************************************************************************************************
void CTFGCServerSystem::AcceptGCReservation( CSteamID steamID, const CTFLobbyMember *pMemberData, bool bIsLateJoin, int nEntindex, bool bActive )
{
if ( m_pMatchInfo )
{
// Accepting new player to competitive match, add to match data
MMLog( "New match player %s\n", steamID.Render() );
m_pMatchInfo->AddPlayer( steamID, pMemberData, bIsLateJoin, nEntindex, bActive );
}
}
// **************************************************************************************************
void CTFGCServerSystem::AbortInvalidMatchState()
{
// TODO ROLLING MATCHES: SteamAPI_SetMiniDumpComment / SteamAPI_WriteMiniDump
MMLog( "**** MM Server in invalid match state, terminating\n" );
engine->ServerCommand( "quit\n" );
}
// **************************************************************************************************
void CTFGCServerSystem::MMServerModeChanged()
{
// Save old boolean state
bool bSaveMMServerMode = m_bMMServerMode;
// Set new state
m_bMMServerMode = ( tf_mm_servermode.GetInt() != 0 );
// Check if logical state is changing; output some text no matter what
if ( m_bMMServerMode )
{
if ( bSaveMMServerMode )
{
MMLog( "Lobby-based matchmaking is active\n" );
}
else
{
MMLog( "Entering lobby-based matchmaking mode\n" );
}
if ( tf_mm_strict.GetInt() == 0 )
{
MMLog( " Open mode active. Gameserver will show in server browser and accept ad-hoc joins.\n" );
}
else if ( tf_mm_strict.GetInt() == 1 )
{
MMLog( " Strict mode is active. Gameserver will not show in server browser or accept ad-hoc joins.\n" );
}
else
{
MMLog( " Server is hidden from server browser list, but will accept ad-hoc joins.\n" );
}
if ( tf_mm_trusted.GetInt() != 0 )
{
MMLog( " Requested trusted server status.\n" );
}
}
else
{
if ( bSaveMMServerMode )
{
MMLog( "Leaving lobby-based matchmaking mode\n" );
}
else
{
MMLog( "Lobby-based matchmaking mode not active\n" );
}
}
// Force this major change out immediately
UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event_None, true );
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CTFGCServerSystem::LaunchNewMatchForLobby()
{
/// XXX(JohnS): Technically the lobby might legitimately be gone here -- if we have gotten the NewMatchForLobby
/// response and the GC then croaks, we might be told it lost our lobby, but have the new match
/// assignment and be able to proceed without needing the lobby at all (as in normal cases where the GC
/// loses state after giving us the authority to run a match).
///
/// Since the match in question hasn't started yet, and this is nearly impossible given the timing
/// window, I'm not doing the work to cache the lobby values we need in here just to let the
/// just-created match survive that edge case.
const CTFGSLobby* pLobby = GetLobby();
if ( !pLobby || m_flWaitingForNewMatchTime == 0.f || !m_pMatchInfo || \
m_pMatchInfo->BMatchTerminated() || m_pMatchInfo->m_bServerCreated )
{
// You need to prepare for the switch with RequestNewMatchForLobby first. Should not have gotten here if we have
// a terminated or server created match -- Must still be managed by the GC in order to roll into a new match.
Assert( false );
MMLog( "!! Attempting to launch a new match for a lobby without valid state\n" );
AbortInvalidMatchState();
}
m_flWaitingForNewMatchTime = 0.f;
CMatchInfo* pNewMatchInfo = new CMatchInfo( pLobby );
// The old match info is holding the vote-winning map name
pNewMatchInfo->m_strMapName = m_pMatchInfo->m_strMapName;
EMatchGroup eMatchGroup = pLobby->GetMatchGroup();
// We still need a new match ID from the GC. Mark that this new match is
// created by us so that: 1) If we do get a response for a new match ID
// we know what to do with it
// 2) The GC knows to assign it a match ID if it
// gets a match result for it before (1) occurs
if ( m_bWaitingForNewMatchID )
{
// Mark that we're going rogue
pNewMatchInfo->m_bServerCreated = true;
pNewMatchInfo->m_nMatchID = 0; // Don't inherit the stale one from the lobby
if ( !CanChangeMatchPlayerTeams() )
{
// Server created speculative matches are counting on the GC approving this when it wakes up, and also
// approving our override of player teams below. If we want a mode that does rolling matches but has no
// authority to override teams, we'd need to just cancel the pending match here instead of using
// m_bServerCreated
AbortInvalidMatchState();
}
}
for( int idx = 0; idx < m_pMatchInfo->GetNumTotalMatchPlayers(); idx++ )
{
const CMatchInfo::PlayerMatchData_t* pPlayerMatchData = m_pMatchInfo->GetMatchDataForPlayer( idx );
// We don't need record of dropped players for the new match
if ( pPlayerMatchData->bDropped )
{ continue; }
// We stop doing maintenance on lobby->match sync during the pending-new-match period, but we don't want to
// include players who would be dropped on the first think -- we'd have erroneous record that they were
// officially part of the match for some period, when they were not.
//
// XXX(JohnS): Technically, we could create a speculative match, then when the new match ID arrives, some
// members vanished -- those members were never actually part of the lobby from the GC
// perspective. We might need to cull these people on the first post-new-matchID-think if having
// record of them is causing problems. (a bWasEverConfirmedByGC flag?)
if ( !pLobby->GetMemberDetails( pPlayerMatchData->steamID ) )
{ continue; }
// AddPlayer needs to know if they are connected/active right now
int nEntIndex = 0;
bool bActive = false;
if ( pPlayerMatchData->bConnected )
{
if ( pPlayerMatchData->nConnectingButNotActiveIndex )
{
// Connected, not active
bActive = false;
nEntIndex = pPlayerMatchData->nConnectingButNotActiveIndex;
}
else
{
// Connected and active
bActive = true;
// We could null check this but we'd just use the information to call AbortInvalidMatchState().
nEntIndex = UTIL_PlayerBySteamID( pPlayerMatchData->steamID )->entindex();
}
}
pNewMatchInfo->AddPlayer( *pPlayerMatchData, nEntIndex, bActive );
}
delete m_pMatchInfo;
m_pMatchInfo = pNewMatchInfo;
// If we are going ahead with a server-created match, queue a ChangeMatchPlayerTeams message in sequence with our
// pending new match request -- the GC will process, in order:
//
// - Give us a new match!
// -> Okay here's new match & teams
// - Set everyone's teams to (the previous match teams)!
// -> Okay here's new lobby with teams that match your state
//
// ... And since we don't run UpdateConnectedPlayers() while messages are in queue, by time we run our next
// look-at-the-lobby think, we'll be in sync again.
CUtlVector< PlayerTeamPair_t > vecPlayerTeams;
for( int idx = 0; idx < m_pMatchInfo->GetNumTotalMatchPlayers(); idx++ )
{
const CMatchInfo::PlayerMatchData_t *pPlayer = m_pMatchInfo->GetMatchDataForPlayer( idx );
vecPlayerTeams.AddToTail( { pPlayer->steamID, pPlayer->eGCTeam } );
}
ChangeMatchPlayerTeams( vecPlayerTeams );
GTFGCClientSystem()->DumpLobby();
if ( eMatchGroup == EMatchGroup::k_nMatchGroup_Invalid ||
!GetMatchGroupDescription( eMatchGroup )->InitServerSettingsForMatch( pLobby ) )
{
AbortInvalidMatchState();
}
}
//-----------------------------------------------------------------------------
// Purpose: Activate / deactive GC hosting mode
//-----------------------------------------------------------------------------
void OnMMServerModeChanged( IConVar *pConVar, const char *pOldString, float flOldValue )
{
GTFGCClientSystem()->MMServerModeChanged();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void OnMMServerModeTrustedChanged( IConVar *pConVar, const char *pOldString, float flOldValue )
{
OnMMServerModeChanged( pConVar, pOldString, flOldValue );
}
ConVar tf_mm_servermode( "tf_mm_servermode", "0", FCVAR_NOTIFY,
"Activates / deactivates Lobby-based hosting mode.\n"
" 0 = not active\n"
" 1 = Put in matchmaking pool (Lobby will control current map)\n",
true,
0.f,
true,
1.f,
OnMMServerModeChanged );
ConVar tf_mm_strict( "tf_mm_strict", "0", FCVAR_NOTIFY,
" 0 = Show in server browser, and allow ad-hoc joins\n"
" 1 = Hide from server browser and only allow joins coordinated through GC matchmaking\n"
" 2 = Hide from server browser, but allow ad-hoc joins\n",
OnMMServerModeChanged );
ConVar tf_mm_trusted( "tf_mm_trusted", "0", FCVAR_NOTIFY | FCVAR_HIDDEN,
"Set to 1 on Valve servers to requested trusted status. (Yes, it is authenticated on the backend, and attempts by non-valve servers are logged.)\n",
OnMMServerModeTrustedChanged );
#endif // #ifdef ENABLE_GC_MATCHMAKING