//========= Copyright Valve Corporation, All rights reserved. ============// #ifndef TF_GC_SERVER_H #define TF_GC_SERVER_H #ifdef _WIN32 #pragma once #endif #if !defined( _X360 ) && !defined( NO_STEAM ) #include "steam/steam_api.h" #include "steam/steam_gameserver.h" #endif //#include "tf_gc_common.h" #include "gcsdk/gcclientsdk.h" #include "playergroup.h" //#include "dota_gamerules.h" #include "gc_clientsystem.h" #include "tf_gcmessages.h" #include "GameEventListener.h" #include "rtime.h" #include "tf_shareddefs.h" class CTFGSLobby; class CTFParty; //enum EDOTA_Uploading_Match_Stats //{ // EDOTA_MATCH_STATS_IDLE, // EDOTA_MATCH_STATS_UPLOADING, // EDOTA_MATCH_STATS_UPLOAD_COMPLETE //}; #ifdef ENABLE_GC_MATCHMAKING class CMvMVictoryInfo { public: int m_nLobbyId; CUtlString m_sChallengeName; #ifdef USE_MVM_TOUR CUtlString m_sMannUpTourOfDuty; #endif // USE_MVM_TOUR CUtlVector m_vPlayerIds; CUtlVector m_vSquadSurplus; RTime32 m_tEventTime; void Init ( CTFGSLobby *pLobby ); }; class CMatchInfo { friend class CTFGCServerSystem; public: CMatchInfo( const CTFGSLobby *pLobby ); ~CMatchInfo(); uint64 m_nMatchID; uint64 m_nLobbyID; EMatchGroup m_eMatchGroup; uint32 m_uLobbyFlags; uint32 m_uAverageRank; RTime32 m_rtMatchCreated; uint32 m_unEventTeamStatus; bool m_bFirstPersonActive; int m_nBotsAdded; bool m_bServerCreated; struct DailyStatsRankBucket_t { uint32 nRank; uint32 nRecords; uint32 nAvgScore; uint32 nStDevScore; uint32 nAvgKills; uint32 nStDevKills; uint32 nAvgDamage; uint32 nStDevDamage; uint32 nAvgHealing; uint32 nStDevHealing; uint32 nAvgSupport; uint32 nStDevSupport; }; struct PlayerMatchData_t { friend class CTFGCServerSystem; friend class CMatchInfo; PlayerMatchData_t( CSteamID steamID, const CTFLobbyMember *pMemberData ) : steamID( steamID ) , uPartyID( pMemberData->party_id() ) , eGCTeam( pMemberData->team() ) , bDropped( false ) , bConnected( false ) , rtJoinedMatch( CRTime::RTime32TimeCur() ) , nVoteKickAttempts( 0 ) , nDisconnectedSeconds( 0 ) , nScoreMedal( 0 ) , nKillsMedal( 0 ) , nDamageMedal( 0 ) , nHealingMedal( 0 ) , nSupportMedal( 0 ) , bLateJoin( false ) , nScore( 0 ) , bPlayed( false ) , unMMSkillRating( 0u ) , nDrilloRatingDelta( 0 ) , unClassesPlayed( 0u ) , rtLastActiveEvent( CRTime::RTime32TimeCur() ) , bAlwaysSafeToLeave( false ) , bEverConnected( false ) , bDropWasAbandon( false ) , eDropReason( TFMatchLeaveReason_UNSPECIFIED ) , nConnectingButNotActiveIndex( 0 ) , m_mapXPAccumulation( DefLessFunc( CMsgTFXPSource::XPSourceType ) ) {} PlayerMatchData_t( const PlayerMatchData_t& rhs ); CSteamID steamID; uint64 uPartyID; TF_GC_TEAM eGCTeam; // If true, this player was dropped from the match and is not part of the active lobby. This is important for // cases where the GC connection is lost and the lobby state is stale. bool bDropped; bool bConnected; // Timestamp player joined the match at. Not guaranteed to be the same instant the match was created, depending // on how the GC does things. RTime32 rtJoinedMatch; uint32 nVoteKickAttempts; // Number of cumulative seconds the player has been absent, *not* including the initial connect timeout. Used // to determine when to award an abandon. We may do odd things like "comp" you some seconds on a second, later, // disconnect, so this shouldn't be used for stats purposes. int nDisconnectedSeconds; int nScoreMedal; int nKillsMedal; int nDamageMedal; int nHealingMedal; int nSupportMedal; bool bLateJoin; int nScore; bool bPlayed; // This is a single-value skill rating given to each player by the GC uint32 unMMSkillRating; // This is the older drillo rating system that was done on the server. It is still sent up to the GC as the // input to the drillo backend there. If we want to keep this long-term it should be moved to be a fully-gc // backend like glicko int nDrilloRatingDelta; uint32 unClassesPlayed; const CMsgTFXPSourceBreakdown& GetXPSources() const { return m_XPBreakdown; } // Override releasing this player from obligation to this match beyond the normal abandon rules. Used by MvM mode // for marking everyone who completes a wave as allowed to drop without penalty, for instance. void MarkAlwaysSafeToLeave() { bAlwaysSafeToLeave = true; } bool BDropWasAbandon() { return bDropped && bDropWasAbandon; } TFMatchLeaveReason GetDropReason() { return bDropped ? eDropReason : TFMatchLeaveReason_UNSPECIFIED; } RTime32 GetLastActiveEventTime( void ) { return rtLastActiveEvent; } MM_PlayerConnectionState_t GetConnectionState() const; void UpdateClassesPlayed( int nClass ); struct XPBonusPool_t { XPBonusPool_t() : m_flMultiplier( 1.f ) , m_nBonusPoolRemaining( 0 ) {} CMsgTFXPSource_XPSourceType m_eType; // Only give up to this amount int m_nBonusPoolRemaining; // Give at this rate float m_flMultiplier; }; private: void OnConnected( int nEntindex ); void OnActive(); // Last time the player changed between active (fully loaded in) and not-active. A player is active if // ( bConnected && !nConnectingButNotActiveIndex ) RTime32 rtLastActiveEvent; bool bAlwaysSafeToLeave; bool bEverConnected; // If dropped - was it an abandon and what was the reason. bool bDropWasAbandon; TFMatchLeaveReason eDropReason; // Track the janky source-engine state between ClientConnect (when we allow them in) and ClientActive int nConnectingButNotActiveIndex; // XP accumulation for a player CMsgTFXPSourceBreakdown m_XPBreakdown; // The breakdown stores ints, but we need float precision or else we're going to round off a // significant amount of XP as the match plays on. CUtlMap< CMsgTFXPSource::XPSourceType, float > m_mapXPAccumulation; CUtlVector< XPBonusPool_t > m_vecXPBonusPools; }; enum RankStatType_t { RankStat_Invalid = -1, RankStat_Score = 0, RankStat_Kills, RankStat_Damage, RankStat_Healing, RankStat_Support, }; void SetDailyRankData( DailyStatsRankBucket_t vecRankData ); bool RequestGCRankData( void ); bool CalculatePlayerMatchRankData( void ); bool CalculateMatchSkillRatingAdjustments( int iWinningTeam ); const CMatchInfo::PlayerMatchData_t* GetMatchDataForPlayer( CSteamID steamID ) const; CMatchInfo::PlayerMatchData_t* GetMatchDataForPlayer( CSteamID steamID ); // For iterating over all players. Index is relative to GetNumTotalMatchPlayers CMatchInfo::PlayerMatchData_t* GetMatchDataForPlayer( int nPlayer ); // This is the total number of players we have match data for -- it may include dropped players not part of the // match any longer. int GetNumTotalMatchPlayers() const; // Number of players still active in the match. int GetNumActiveMatchPlayers() const; // Number of players still active in the match for a specific team int GetNumActiveMatchPlayersForTeam( int nTeam ) const; // Total skill rating for a team int GetTotalSkillRatingForTeam( int nTeam ) const; // Subset of active match players who are currently connected int GetNumConnectedMatchPlayers() const; // Indicates that this is a stale match that is ending. In cases such as rolling matches, we never "end" a match, // just roll into the next one, since "ended" matches indicate that we've terminated our relationship with the // players/GC. bool BMatchTerminated() const { return m_bMatchEnded; } // Indicates we've sent a result for this match. The match may still be active if we're intending to use it to start // a rolling match or other post-game activities. bool BSentResult() const { return m_bSentResult; } // The canonical size of this type of match. Can be passed from the GC and override the match description size. uint32 GetCanonicalMatchSize() const; const char *GetMatchMap() const { return m_strMapName.Length() ? m_strMapName.Get() : NULL; } // Rewards the player with XP based on the count scaled by the XP per unit of that type. // nCount here is the raw occurances of the action (ie. Points scored, Gold Medals Scored) void GiveXPRewardToPlayerForAction( CSteamID steamID, CMsgTFXPSource::XPSourceType eType, int nCount ); // Directly assign the value void GiveXPDirectly( CSteamID steamID, CMsgTFXPSource::XPSourceType eType, int nAmount, bool bCanAwardBonusXP = true ); // Give an XP bonus that increases void GiveXPBonus( CSteamID steamID, CMsgTFXPSource_XPSourceType eType, float flMultipler, int nBonusPool ); // Is this player allowed to leave the match without incurring an abandon right now // TODO(JohnS): This should not go from true to false due to race conditions (players clicks DC, sees no warning, // races with server deciding its unsafe again), but does for MvM late join. The GS-initiated late // join rework would make it possible to fix that (once it enters too-low-to-latejoin state it stays // there) bool BPlayerSafeToLeaveMatch( CSteamID steamID ); protected: int GetRankForStat( RankStatType_t statType, int nRankIndex, uint32 nValue ); float NormalDistributionCDF( float flValue, float flMu, float flSigma ); private: CMatchInfo(); CMatchInfo( const CMatchInfo &otherinfo ); // Track a new player participating in our match void AddPlayer( CSteamID steamID, const CTFLobbyMember *pMemberData, bool bIsLateJoin, int nEntindex, bool bActive ); // Or with an existing player to copy from (e.g. old match) void AddPlayer( const PlayerMatchData_t &oldPlayer, int nEntIndex, bool bActive ); // Mark a player as dropped from the match void DropPlayer( CSteamID steamID, TFMatchLeaveReason eReason, bool bWasAbandon ); void SetEnded() { m_bMatchEnded = true; } CUtlVector < DailyStatsRankBucket_t > m_vDailyStatsRankData; CUtlVector < PlayerMatchData_t* > m_vMatchRankData; CUtlString m_strMapName; bool m_bMatchEnded; bool m_bSentResult; // Canonical size for this match type, override passed from GC. uint32 m_nGCMatchSize; float m_flBronzePercentile; float m_flSilverPercentile; float m_flGoldPercentile; }; class CTFGCServerSystem : public CGCClientSystem, public GCSDK::ISharedObjectListener, public IServerGCLobby, public CGameEventListener { DECLARE_CLASS_GAMEROOT( CTFGCServerSystem, CGCClientSystem ); // Messages that need to do callbacks friend class ReliableMsgNewMatchForLobby; friend class ReliableMsgChangeMatchPlayerTeams; public: CTFGCServerSystem( void ); ~CTFGCServerSystem( void ); // CAutoGameSystemPerFrame virtual bool Init() OVERRIDE; virtual void LevelInitPreEntity() OVERRIDE; virtual void LevelShutdownPostEntity() OVERRIDE; virtual void Shutdown() OVERRIDE; virtual void PreClientUpdate() OVERRIDE; // uint8 FindItemID( CTF_Item *pItem ); void MatchSignOut(); // const char *GetMatchStartTimeString(); // void GameRules_State_Enter( DOTA_GameState newState ); void SetHibernation( bool bHibernating ); bool ShouldHideServer(); // ISharedObjectListener virtual void SOCreated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) OVERRIDE; virtual void PreSOUpdate( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent ) OVERRIDE { /* do nothing */ } virtual void SOUpdated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) OVERRIDE; virtual void PostSOUpdate( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent ) OVERRIDE { /* do nothing */ } virtual void SODestroyed( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) OVERRIDE; virtual void SOCacheSubscribed( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent ) OVERRIDE { } virtual void SOCacheUnsubscribed( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent ) OVERRIDE { } void DumpLobby(); // IServerGCLobby methods virtual bool HasLobby() const; virtual bool SteamIDAllowedToConnect(const CSteamID &steamId) const; virtual void UpdateServerDetails(void); virtual bool ShouldHibernate(); // IGameEventListener2 virtual void FireGameEvent( IGameEvent *event ) OVERRIDE; CTFParty* GetPartyForPlayer( CSteamID steamID ) const; CMatchInfo *GetMatch() { return m_pMatchInfo; } const CMatchInfo *GetMatch() const { return m_pMatchInfo; } // Verbose accessor helpers // // Get match only if it is live CMatchInfo *GetLiveMatch() { return ( m_pMatchInfo && !m_pMatchInfo->m_bMatchEnded ) ? m_pMatchInfo : NULL; } const CMatchInfo *GetLiveMatch() const { return const_cast(this)->GetLiveMatch(); } // Get a player only if there is a live match and they are still in the match (not dropped) CMatchInfo::PlayerMatchData_t *GetLiveMatchPlayer( CSteamID steamID ); const CMatchInfo::PlayerMatchData_t *GetLiveMatchPlayer( CSteamID steamID ) const ; int GetTeamForLobbyMember( const CSteamID &steamId ) const; // bool IsLobbyMemberBroadcaster( const CSteamID &steamId ) const; // ELanguage GetBroadcasterLanguage( const CSteamID &steamId ) const; float GetFirstConnectTimeForLobbyMember( const CSteamID &steamId ) const; int GetVoteKickAttemptsByLobbyMember( const CSteamID &steamID ) const; void IncrementVoteKickAttemptsByLobbyMember( const CSteamID &steamID ); // // EDOTA_Uploading_Match_Stats UploadingMatchStats() { return m_nUploadingMatchStats; } // void OnStatsSubmitted( uint32 unMatchID, int32 nReplaySalt ); // uint32 GetLastMatchID() { return m_unLastMatchID; } // int32 GetLastReplaySalt() { return m_nLastReplaySalt; } void ClientActive( CSteamID steamIDClient ); void ClientConnected( CSteamID steamIDPlayer, edict_t *pEntity ); void ClientDisconnected( CSteamID steamIDClient ); inline bool IsMMServerModeActive() const { return m_bMMServerMode; } void MMServerModeChanged(); // void SetRelayedGameServerSteamID( const CSteamID &steamID ) { m_relayedGameServerSteamID = steamID; } // void SetParentRelayCount( int nParentRelayCount ) { m_nParentRelayCount = nParentRelayCount; } float GetTimeLastConnectedToGC( void ) { return m_timeLastConnectedToGC; } void EndManagedMatch( bool bKickPlayersToParties = false ); // Sends match results. Expects the managed match be ended. /// MvM game rules processing lets us the players have won void SendMvMVictoryResult(); // Takes ownership of matchResultMsg void SendCompetitiveMatchResult( GCSDK::CProtoBufMsg< CMsgGC_Match_Result > *matchResultMsg ); // If the GC has confirmed we are in the pool for late joins. GetTimeRequestedLateJoin() can be compared with // BLateJoinEligible() to reason about delays in the GC making us available for late join. bool BLateJoinEligible(); double GetTimeRequestedLateJoin() { return m_flTimeRequestedLateJoin; } // Eject a player from the match, kicking them if they are still present, with given reason. bool EjectMatchPlayer( CSteamID steamID, TFMatchLeaveReason eReason ); void MatchPlayerVoteKicked( CSteamID steamID ); const MapDef_t* GetNextMapVoteByIndex( int nIndex ) const; // Changing Match Player Teams // // Is game logic allowed to perform team reassignments for this match mode? bool CanChangeMatchPlayerTeams(); // When game logic changes the team of a match player, this should be called immediately. It is invalid to call this // if CanChangeMatchPlayerTeams is false. void ChangeMatchPlayerTeam( CSteamID steamID, TF_GC_TEAM eTeam ); // Multi-player version of the above, less GC traffic when multiple reassignments occur at once. struct PlayerTeamPair_t { CSteamID steamID; TF_GC_TEAM eTeam; }; template< typename ANY_ALLOCATOR > void ChangeMatchPlayerTeams( const CUtlVector< PlayerTeamPair_t, ANY_ALLOCATOR > &vecNewTeams ); // Rolling Matches // // Some match types let us keep a lobby and roll into a new match. This may not always be allowed depending on GC // state. // // Upon calling RequestNewMatchForLobby, a timer starts, after which the new match is launched in // LaunchNewMatchForLobby. During this period our match object is the old match, but the lobby may have been updated // to reflect the new match. // // !! Currently if the GC is out of contact, we will speculatively continue with a new match. There are rare cases // where the GC will return and decline this request, in which case the resulting match will be unofficial and // not recorded. It should be trivial to add a bool to prevent such speculative matches should it be necessary. bool CanRequestNewMatchForLobby(); void RequestNewMatchForLobby( const MapDef_t* pNewMap ); // If the match is in a bogus state and has no useful resolution, terminate it and submit a minidump. This usually // just means reboot the match server. void AbortInvalidMatchState(); protected: // CGCClientSystem virtual void PreInitGC() OVERRIDE; virtual void PostInitGC() OVERRIDE; private: void SendPlayerLeftMatch( CSteamID steamID, TFMatchLeaveReason eReason, bool bAbandoned ); // Send a kick-lobby message for a stale or unexpected lobby void SendRejectLobby(); // Kick a player that is no longer present in the match and should not be here. // Returns true if they were present bool KickRemovedMatchPlayer( CSteamID steamIDClient ); // The lobby object should only be looked at by this class and may be out of date when the GC reboots and similar -- // most users should use the canonical match state in GetMatch() const CTFGSLobby *GetLobby() const; CTFGSLobby *GetLobby(); // Accepts a reservation request from the GC, adding this player to our reserved list, and, for MM mode, to the // match. void AcceptGCReservation( CSteamID steamID, const CTFLobbyMember *pMemberData, bool bIsLateJoin, int nEntindex, bool bActive ); // Rolling Matches (private) // // If we requested a new match for our existing lobby. We don't actually launch the new match for this timer, but // the GC may get back to us before (or after!) that period, so this tracks that we're not currently in sync with // our lobby object. See RequestNewMatchForLobby / LaunchNewMatchForLobby and the "Team Assignments" section of the // big comment at the top of tf_gc_server.cpp bool BPendingNewMatch() const { return m_flWaitingForNewMatchTime != 0.f; } // Called at the end of the m_flWaitingForNewMatchTime period to actually create the new match. We could let the // caller finish the launch process by changing this timer to a bool and making this public. void LaunchNewMatchForLobby(); // Callbacks from the GC void ChangeMatchPlayerTeamsResponse( bool bSuccess ); void NewMatchForLobbyResponse( bool bSuccess ); // Static callbacks that are just forwarding to us static void ChangeMatchPlayerTeamsResponseCallback( GCSDK::CProtoBufMsg& msg ); static void NewMatchForLobbyResponseCallback( GCSDK::CProtoBufMsg& msg ); bool m_bSetupSchema; RTime32 m_unGameStartTime; float m_timeLastSendGameServerInfoAndConnectedPlayers; ServerMatchmakingState m_eLastGameServerUpdateState; TF_MatchmakingMode m_eLastGameServerUpdateMatchmakingMode; CUtlString m_sLastGameServerUpdateMap; CUtlString m_sLastGameServerUpdateTags; int m_nLastGameServerUpdateBotCount; int m_nLastGameServerUpdateMaxHumans; int m_nLastGameServerUpdateSlotsFree; uint32 m_nLastGameServerUpdateLobbyMMVersion; // EDOTA_Uploading_Match_Stats m_nUploadingMatchStats; // uint32 m_unLastMatchID; // int32 m_nLastReplaySalt; CSteamID m_ourSteamID; CSteamID m_relayedGameServerSteamID; int m_nParentRelayCount; bool m_bMMServerMode; double m_flTimeBecameEmptyWithLobby; double m_flTimeRequestedLateJoin; bool m_bLateJoinEligible; int m_iSavedVisibleMaxPlayers; bool m_bOverridingVisibleMaxPlayers; bool m_bWaitingForNewMatchID; float m_flWaitingForNewMatchTime; CMvMVictoryInfo m_mvmVictoryInfo; // Check for match players who have been disconnected for long enough to warrant an abandon and do so. void MatchPlayerAbandonThink(); void SetMatchPlayerDropped( CSteamID steamID, TFMatchLeaveReason eReason ); void UpdateConnectedPlayersAndServerInfo( CMsgGameServerMatchmakingStatus_Event event, bool bForceSendServerInfo ); CMatchInfo *m_pMatchInfo; float m_timeLastConnectedToGC; // DOTAGameVersion m_GameVersion; }; CTFGCServerSystem *GTFGCClientSystem(); #endif // #ifdef ENABLE_GC_MATCHMAKING #endif // TF_GC_SERVER_H