diff options
Diffstat (limited to 'game/server/tf/tf_gc_server.cpp')
| -rw-r--r-- | game/server/tf/tf_gc_server.cpp | 3848 |
1 files changed, 3848 insertions, 0 deletions
diff --git a/game/server/tf/tf_gc_server.cpp b/game/server/tf/tf_gc_server.cpp new file mode 100644 index 0000000..9dd4e2a --- /dev/null +++ b/game/server/tf/tf_gc_server.cpp @@ -0,0 +1,3848 @@ +//========= 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 |