diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/shared/tf/tf_match_description.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/shared/tf/tf_match_description.cpp')
| -rw-r--r-- | game/shared/tf/tf_match_description.cpp | 1783 |
1 files changed, 1783 insertions, 0 deletions
diff --git a/game/shared/tf/tf_match_description.cpp b/game/shared/tf/tf_match_description.cpp new file mode 100644 index 0000000..a9b50a6 --- /dev/null +++ b/game/shared/tf/tf_match_description.cpp @@ -0,0 +1,1783 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#include "cbase.h" +#include "tf_match_description.h" +#include "tf_ladder_data.h" +#include "tf_rating_data.h" + +#ifdef GC_DLL +#include "tf_lobby.h" +#include "tf_partymanager.h" +#endif + +#if defined CLIENT_DLL || defined GAME_DLL +#include "tf_gamerules.h" +#endif + +#ifdef CLIENT_DLL +#include "tf_gc_client.h" +#include "animation.h" +#include "vgui/ISurface.h" +#include "vgui_controls/Controls.h" +#include "tf_lobby_server.h" +#endif + +#ifdef GAME_DLL +#include "tf_lobby_server.h" +#include "tf_gc_server.h" +#include "tf_objective_resource.h" +#include "team_control_point_master.h" +#endif + +#ifdef GC_DLL +GCConVar tf_mm_ladder_force_map_by_name( "tf_mm_ladder_force_map_by_name", "", "If specified, force any matches that form for 6v6, 9v9, 12v12 to use the specified map (e.g. cp_sunshine)." ); +GCConVar tf_mm_casual_rejoin_cooldown_secs( "tf_mm_casual_rejoin_cooldown_secs", "180", + "How many seconds must pass before anyone can re-match into a casual or MvM lobby they have left. " + "Setting this too low may allow someone to rejoin a lobby while a vote-kick for them is still occuring, but before it has passed." ); +GCConVar tf_mm_casual_rejoin_cooldown_votekick_secs( "tf_mm_casual_rejoin_cooldown_votekick_secs", "10800", // 3 hours + "How many seconds must pass before a vote-kicked player can re-match into a casual or MvM lobby. " + "This is not infinite as casual lobbies may persist for days." ); +// These are in matchmaking shared +extern GCConVar tf_mm_match_size_mvm; +extern GCConVar tf_mm_match_size_ladder_6v6; +extern GCConVar tf_mm_match_size_ladder_9v9; +extern GCConVar tf_mm_match_size_ladder_12v12; +extern GCConVar tf_mm_match_size_ladder_12v12_minimum; +#else +extern ConVar tf_mm_match_size_mvm; +extern ConVar tf_mm_match_size_ladder_6v6; +extern ConVar tf_mm_match_size_ladder_9v9; +extern ConVar tf_mm_match_size_ladder_12v12; +extern ConVar tf_mm_match_size_ladder_12v12_minimum; +extern ConVar servercfgfile; +extern ConVar lservercfgfile; +extern ConVar mp_tournament_stopwatch; +extern ConVar tf_gamemode_payload; +extern ConVar tf_gamemode_ctf; +#endif + +#ifdef GAME_DLL +extern ConVar tf_mvm_allow_abandon_after_seconds; +extern ConVar tf_mvm_allow_abandon_below_players; +#endif + +#ifdef GC_DLL + #define MVM_REQUIRED_SCORE &tf_mm_required_score_mvm + #define LADDER_REQUIRED_SCORE &tf_mm_required_score_ladder + #define CASUAL_REQUIRED_SCORE &tf_mm_required_score_ladder +#else + #define MVM_REQUIRED_SCORE (ConVar*)NULL + #define LADDER_REQUIRED_SCORE (ConVar*)NULL + #define CASUAL_REQUIRED_SCORE (ConVar*)NULL +#endif + +#ifdef GC_DLL +#include "tf_matchmaker.h" +#include "tf_party.h" + +using namespace GCSDK; +extern GCConVar tf_mm_scoring_ladder_skillrating_delta_max_high; +extern GCConVar tf_mm_scoring_ladder_skillrating_delta_max; +extern GCConVar tf_mm_required_score_mvm; +extern GCConVar tf_mm_required_score_ladder; +extern GCConVar tf_mm_required_score_pvp; + +// Predicate for below filters +typedef std::function< bool(const CTFMemcachedLobbyFormerMember &) > FilterLeftMember_t; + +// Helper that finds any party members that appear in the given match's m_mapFormerMembersByAccountID as having left the +// match, and calls the given predicate on them. +// +// Returns true if no party members are in the former member list with a match-leave flag, or they all pass the +// predicate. +// +// Returns false and stops iterating if predicate fails. +static bool BCheckLeftMatchMembersAgainstParty( const MatchDescription_t *pMatch, + const MatchParty_t *pParty, + const FilterLeftMember_t &predicate ) +{ + FOR_EACH_VEC( pParty->m_vecMembers, idxMember ) + { + const MatchParty_t::Member_t &member = pParty->m_vecMembers[ idxMember ]; + UtlHashHandle_t idx = pMatch->m_mapFormerMembersByAccountID.Find( member.m_steamID.GetAccountID() ); + if ( pMatch->m_mapFormerMembersByAccountID.IsValidHandle( idx ) ) + { + const CTFMemcachedLobbyFormerMember &formerMember = pMatch->m_mapFormerMembersByAccountID[ idx ]; + // Only care about former members that left a match this lobby was visiting + if ( !formerMember.has_left_match_time() ) + { continue; } + + if ( !predicate( formerMember ) ) + { + return false; + } + } + } + + return true; +} + +// Helper that finds any newly-added (not existing incomplete-match-party) members of the given match description that +// appear in m_mapFormerMembersByAccountID as having left the match previously, and calls the given predicate on them. +// +// Returns true if no new match members appear in the former member list with a match-leave flag, or they all pass the +// predicate. +// +// Returns false and stops iterating if predicate fails. +static bool BCheckLeftMatchMembersAgainstNewlyAddedMembers( const MatchDescription_t *pMatch, + const FilterLeftMember_t &predicate ) +{ + FOR_EACH_VEC( pMatch->m_vecParties, idx ) + { + auto *pParty = pMatch->m_vecParties[ idx ]; + // If this party is not an incomplete match party (and thus already in the match), run the check + if ( !pParty->m_bIncompleteMatchParty && !BCheckLeftMatchMembersAgainstParty( pMatch, pParty, predicate ) ) + { + return false; + } + } + + return true; +} + +// Shared helper for the casual modes to have a consistent rejoin policy +static bool BThreadedFormerMemberMayRejoinJoinCasualOrMvMMatch( const CTFMemcachedLobbyFormerMember& member ) +{ + RTime32 rtNow = CRTime::RTime32TimeCur(); + RTime32 rtLeftLobby = member.left_lobby_time(); + uint32_t unRejoinTimeout = (uint32_t)Max( tf_mm_casual_rejoin_cooldown_secs.GetInt(), 0 ); + + if ( unRejoinTimeout && rtLeftLobby != 0 && + rtLeftLobby + unRejoinTimeout > rtNow ) + { + // Rejoin too soon after leaving lobby + return false; + } + + uint32_t unVoteKickTimeout = (uint32_t)Max( tf_mm_casual_rejoin_cooldown_votekick_secs.GetInt(), 0 ); + RTime32 rtLeftMatch = member.left_match_time(); + TFMatchLeaveReason eLeftMatchReason = member.left_match_reason(); + if ( unVoteKickTimeout && + rtLeftMatch != 0 && + ( eLeftMatchReason == TFMatchLeaveReason_VOTE_KICK || + eLeftMatchReason == TFMatchLeaveReason_ADMIN_KICK ) && + rtLeftMatch + unVoteKickTimeout > rtNow ) + { + // Too soon after votekick + return false; + } + + return true; +} + +int IMatchGroupDescription::GetServerPoolIndex( EMatchGroup eGroup, EMMServerMode eMode ) const +{ + int nResult = (int)eGroup; + switch ( eMode ) + { + case eMMServerMode_Idle: + { + nResult = k_nGameServerPool_Idle; + } + break; + + case eMMServerMode_Full: + { + COMPILE_TIME_ASSERT( (int)k_nMatchGroup_MvM_Practice + (int)k_nGameServerPool_Full_First == (int)k_nGameServerPool_MvM_Practice_Full ); + nResult += k_nGameServerPool_MvM_Practice_Full; + Assert( nResult >= k_nGameServerPool_Full_First ); + Assert( nResult <= k_nGameServerPool_Full_Last ); + return nResult; + } + break; + + case eMMServerMode_Incomplete_Match: + { + COMPILE_TIME_ASSERT( (int)k_nMatchGroup_MvM_Practice + (int)k_nGameServerPool_Incomplete_Match_First == (int)k_nGameServerPool_MvM_Practice_Incomplete_Match ); + nResult += k_nGameServerPool_Incomplete_Match_First; + Assert( nResult >= k_nGameServerPool_Incomplete_Match_First ); + Assert( nResult <= k_nGameServerPool_Incomplete_Match_Last ); + } + break; + + default: + Assert( false ); + } + + return nResult; +} +#endif + +#ifdef GAME_DLL + +bool IMatchGroupDescription::InitServerSettingsForMatch( const CTFGSLobby* pLobby ) const +{ + // Setting servercfgfile to our mode-specific config causes the server to exec it once it finishes + // loading the map from the changelevel below + servercfgfile.SetValue( m_params.m_pszExecFileName ); + lservercfgfile.SetValue( m_params.m_pszExecFileName ); + + return TFGameRules()->StartManagedMatch(); +} +#endif + +#ifdef CLIENT_DLL + +#ifdef STAGING_ONLY +void cc_tf_test_pvp_rank_xp_change( IConVar *pConVar, const char *pOldString, float flOldValue ) +{ + IGameEvent *pEvent = gameeventmanager->CreateEvent( "experience_changed" ); + if ( pEvent ) + { + gameeventmanager->FireEventClientSide( pEvent ); + } +} +ConVar tf_test_pvp_rank_xp_change( "tf_test_pvp_rank_xp_change", "-1", 0, "Force your experience to a specific value", cc_tf_test_pvp_rank_xp_change ); + + +CON_COMMAND_F( tf_progression_set_xp_to_level, "Overrides your XP to be within the range the level specified", FCVAR_CHEAT ) +{ + if ( args.ArgC() != 3 ) + { + Msg( "Usage tf_progression_set_xp_to_level <matchgroup> <level>\n" ); + return; + } + + const IMatchGroupDescription* pMatch = GetMatchGroupDescription( (EMatchGroup)atoi( args[1] ) ); + if ( !pMatch || !pMatch->m_pProgressionDesc ) + return; + + const LevelInfo_t& level = pMatch->m_pProgressionDesc->GetLevelByNumber( atoi( args[2] ) ); + tf_test_pvp_rank_xp_change.SetValue( RandomInt( level.m_nStartXP, level.m_nEndXP ) ); + + IGameEvent *pEvent = gameeventmanager->CreateEvent( "experience_changed" ); + if ( pEvent ) + { + gameeventmanager->FireEventClientSide( pEvent ); + } + + pEvent = gameeventmanager->CreateEvent( "begin_xp_lerp" ); + if ( pEvent ) + { + gameeventmanager->FireEventClientSide( pEvent ); + } +} + +CON_COMMAND_F( tf_progression_set_xp_to_value, "Overrides your XP to be a specific value", FCVAR_CHEAT ) +{ + if ( args.ArgC() != 3 ) + { + Msg( "Usage tf_progression_set_xp_to_level <matchgroup> <value>\n" ); + return; + } + + const IMatchGroupDescription* pMatch = GetMatchGroupDescription( (EMatchGroup)atoi( args[1] ) ); + if ( !pMatch || !pMatch->m_pProgressionDesc ) + return; + + tf_test_pvp_rank_xp_change.SetValue( atoi( args[2] ) ); + + IGameEvent *pEvent = gameeventmanager->CreateEvent( "experience_changed" ); + if ( pEvent ) + { + gameeventmanager->FireEventClientSide( pEvent ); + } + + pEvent = gameeventmanager->CreateEvent( "begin_xp_lerp" ); + if ( pEvent ) + { + gameeventmanager->FireEventClientSide( pEvent ); + } +} +#endif // STAGING_ONLY +#endif // CLIENT_DLL + +// Casual XP constants +const float flAverageXPPerGame = 500.f; +const float flAverageMinutesPerGame = 30; + +// The target XP per minute +const float flTargetXPPM = (float)flAverageXPPerGame / (float)flAverageMinutesPerGame; + +// The target breakdown at the end of a match +const float flScoreXPScale = 0.4485f; +const float flObjectiveXPScale = 0.15f; +const float flMatchCompletionXPScale = 0.3f; + +// These come from the first 4 weeks of MyM match data +const float flAvgPPMPM = 27.f; // Points per minute per match +const float flAvgPPMPP = 1.15f; // Points per minute per player (above / 24) + +const XPSourceDef_t g_XPSourceDefs[] = { { "MatchMaking.XPChime", "TF_XPSource_PositiveFormat", "#TF_XPSource_Score", flTargetXPPM * flScoreXPScale / flAvgPPMPP /* 6.5 */ } // SOURCE_SCORE = 0; + , { "MatchMaking.XPChime", "TF_XPSource_PositiveFormat", "#TF_XPSource_ObjectiveBonus", flTargetXPPM * flObjectiveXPScale / flAvgPPMPM /* 0.0926 */ } // SOURCE_OBJECTIVE_BONUS = 1; + , { "MatchMaking.XPChime", "TF_XPSource_PositiveFormat", "#TF_XPSource_CompletedMatch", flTargetXPPM * flMatchCompletionXPScale / flAvgPPMPM /* 0.185 */ } // SOURCE_COMPLETED_MATCH = 2; + , { "MVM.PlayerDied", "TF_XPSource_NoValueFormat", "#TF_XPSource_Comp_Abandon", 1.f } // SOURCE_COMPETITIVE_ABANDON = 3; + , { "MatchMaking.XPChime", "TF_XPSource_NoValueFormat", "#TF_XPSource_Comp_Win", 1.f } // SOURCE_COMPETITIVE_WIN = 4; + , { NULL, "TF_XPSource_NoValueFormat", "#TF_XPSource_Comp_Loss", 1.f } // SOURCE_COMPETITIVE_LOSS = 5; + , { "MatchMaking.XPChime", "TF_XPSource_PositiveFormat", "#TF_XPSource_Autobalance_Bonus", 1.f } }; // SOURCE_AUTOBALANCE_BONUS = 6; + +IProgressionDesc::IProgressionDesc( EMatchGroup eMatchGroup + , const char* pszBadgeName + , const char* pszProgressionResFile + , const char* pszLevelToken ) + : m_eMatchGroup( eMatchGroup ) + , m_strBadgeName( pszBadgeName ) + , m_pszProgressionResFile( pszProgressionResFile ) + , m_pszLevelToken( pszLevelToken ) +{} + + +#ifdef CLIENT_DLL +void IProgressionDesc::EnsureBadgePanelModel( CBaseModelPanel *pModelPanel ) const +{ + studiohdr_t* pHDR = pModelPanel->GetStudioHdr(); + if ( !pHDR || !(CUtlString( pHDR->name ).UnqualifiedFilename() == m_strBadgeName.UnqualifiedFilename()) ) + { + pModelPanel->SetMDL( m_strBadgeName ); + } +} + +const LevelInfo_t& IProgressionDesc::YieldingGetLevelForSteamID( const CSteamID& steamID ) const +{ + return GetLevelForExperience( GetPlayerExperienceBySteamID( steamID ) ); +} +#endif // CLIENT_DLL + +const LevelInfo_t& IProgressionDesc::GetLevelByNumber( uint32 nNumber ) const +{ + int nIndex = nNumber; + nIndex = Clamp( nIndex - 1, 0, m_vecLevels.Count() - 1 ); + Assert( nIndex >= 0 && nIndex < m_vecLevels.Count() ); + return m_vecLevels[ nIndex ]; +}; + +const LevelInfo_t& IProgressionDesc::GetLevelForExperience( uint32 nExperience ) const +{ + uint32 nNumLevels = (uint32)m_vecLevels.Count(); + // Walk the levels to find where the passed in experience value falls + for( uint32 i=0; i<nNumLevels; ++i ) + { + if ( nExperience >= m_vecLevels[ i ].m_nStartXP && ( nExperience < m_vecLevels[ i ].m_nEndXP || (i + 1) == nNumLevels ) ) + { + return m_vecLevels[ i ]; + } + } + + Assert( false ); + return m_vecLevels[ 0 ]; +} + +class CMvMMatchGroupDescription : public IMatchGroupDescription +{ + public: + CMvMMatchGroupDescription( EMatchGroup eMatchGroup, const char* pszConfig, bool bTrustedOnly ) + : IMatchGroupDescription( eMatchGroup + , { eMatchMode_MatchMaker_LateJoinDropIn // m_eLateJoinMode; + , eMMPenaltyPool_Casual // m_ePenaltyPool + , false // m_bUsesSkillRatings; + , false // m_bSupportsLowPriorityQueue; + , false // m_bRequiresMatchID; + , MVM_REQUIRED_SCORE // m_pmm_required_score; + , false // m_bUseMatchHud; + , pszConfig // m_pszExecFileName; + , &tf_mm_match_size_mvm // m_pmm_match_group_size; + , NULL // m_pmm_match_group_size_minimum; + , MATCH_TYPE_MVM // m_eMatchType; + , false // m_bShowPreRoundDoors; + , false // m_bShowPostRoundDoors; + , NULL // m_pszMatchEndKickWarning; + , NULL // m_pszMatchStartSound; + , false // m_bAutoReady; + , false // m_bShowRankIcons; + , false // m_bUseMatchSummaryStage; + , false // m_bDistributePerformanceMedals; + , false // m_bIsCompetitiveMode; + , false // m_bUseFirstBlood; + , false // m_bUseReducedBonusTime; + , false // m_bUseAutoBalance; + , false // m_bAllowTeamChange; + , true // m_bRandomWeaponCrits; + , false // m_bFixedWeaponSpread; + , false // m_bRequireCompleteMatch; + , bTrustedOnly // m_bTrustedServersOnly; + , false // m_bForceClientSettings; + , false // m_bAllowDrawingAtMatchSummary + , true // m_bAllowSpecModeChange + , false // m_bAutomaticallyRequeueAfterMatchEnds + , false // m_bUsesMapVoteOnRoundEnd + , false // m_bUsesXP + , false // m_bUsesDashboardOnRoundEnd + , false // m_bUsesSurveys + , false } ) // m_bStrictMatchmakerScoring + {} + +#ifdef GC_DLL + virtual EMMRating PrimaryMMRatingBackend() const OVERRIDE { return k_nMMRating_Invalid; } + virtual const std::vector< EMMRating > &MatchResultRatingBackends() const OVERRIDE + { + static std::vector< EMMRating > mvmRatings = { /* crickets */ }; + return mvmRatings; + } + + // Copy the party's search challenges + bool InitMatchFromParty( MatchDescription_t* pMatch, const MatchParty_t* pParty ) const OVERRIDE + { + pMatch->m_setAcceptableChallenges = pParty->m_setSearchChallenges; + +#ifdef USE_MVM_TOUR + if ( pMatch->m_eMatchGroup == k_nMatchGroup_MvM_MannUp ) + { + Assert( pParty->m_iMannUpTourOfDuty >= 0 ); + pMatch->m_iMannUpTourOfDuty = pParty->m_iMannUpTourOfDuty; + } + else + { + Assert( pParty->m_iMannUpTourOfDuty == k_iMvmTourIndex_NotMannedUp ); + pMatch->m_iMannUpTourOfDuty = k_iMvmTourIndex_NotMannedUp; + } +#endif // USE_MVM_TOUR + return true; + } + + virtual bool InitMatchFromLobby( MatchDescription_t* pMatch, CTFLobby* pLobby ) const OVERRIDE + { + pMatch->m_setAcceptableChallenges.Clear(); + pMatch->m_setAcceptableChallenges.SetMissionBySchemaIndex( pLobby->GetMissionIndex(), true ); + +#ifdef USE_MVM_TOUR + if ( pLobby->GetMatchGroup() == k_nMatchGroup_MvM_MannUp ) + { + pMatch->m_iMannUpTourOfDuty = pLobby->GetMannUpTourIndex(); + } + else + { + Assert( pLobby->GetMannUpTourIndex() == k_iMvmTourIndex_NotMannedUp ); + } +#endif // USE_MVM_TOUR + + return true; + } + + // Sync selected MvM challenges + virtual void SyncMatchParty( const CTFParty *pParty, MatchParty_t *pMatchParty ) const OVERRIDE + { + pParty->GetSearchChallenges( pMatchParty->m_setSearchChallenges ); + } + + // Go through our selected challenges and pick a random challenge then a random popfile from the chosen challenge + virtual void SelectModeSpecificParameters( const MatchDescription_t* pMatch, CTFLobby* pLobby ) const OVERRIDE + { + if ( !pMatch ) + return; + + int nCountPops = GetItemSchema()->GetMvmMissions().Count(); + int nSelectedChallenge = -1; + nSelectedChallenge = -1; + + int n = 0; + for ( int i = 0 ; i < nCountPops ; ++i ) + { + if ( pMatch->m_setAcceptableChallenges.GetMissionBySchemaIndex( i ) ) + { + int r = RandomInt( 0, n ); + if ( r == 0 ) + { + nSelectedChallenge = i; + } + ++n; + } + } + + // We *should* have chosen one by now, unless the schema is hosed. + // But if we haven't, force it to be selected now + if ( nSelectedChallenge < 0 ) + { + Assert( nSelectedChallenge >= 0 ); + nSelectedChallenge = RandomInt( 0, nCountPops-1 ); + } + + Assert( nSelectedChallenge >= 0 ); + pLobby->SetMapName( GetItemSchema()->GetMvmMissions()[ nSelectedChallenge ].m_sMapNameActual.Get() ); + pLobby->SetMissionName( GetItemSchema()->GetMvmMissions()[ nSelectedChallenge ].m_sPop.Get() ); +#ifdef USE_MVM_TOUR + if ( pMatch->m_eMatchGroup == k_nMatchGroup_MvM_MannUp ) + { + Assert( pMatch->m_iMannUpTourOfDuty >= 0 ); + pLobby->SetMannUpTourName( GetItemSchema()->GetMvmTours()[pMatch->m_iMannUpTourOfDuty].m_sTourInternalName.Get() ); + } + else + { + Assert( pMatch->m_iMannUpTourOfDuty == k_iMvmTourIndex_NotMannedUp ); + } +#endif // USE_MVM_TOUR + } + + // Check MvM challenges have an intersection between the current and searching parties + virtual bool BThreadedPartyCompatibleWithMatch( const MatchDescription_t* pMatch, const MatchParty_t *pCandidateParty ) const OVERRIDE + { + // Check for blacklisted former members that tank compatibility + if ( !BCheckLeftMatchMembersAgainstParty( pMatch, pCandidateParty, &BThreadedFormerMemberMayRejoinJoinCasualOrMvMMatch) ) + { return false; } + +#ifdef USE_MVM_TOUR + return pCandidateParty->m_iMannUpTourOfDuty == pMatch->m_iMannUpTourOfDuty; +#endif // USE_MVM_TOUR + + return pMatch->m_setAcceptableChallenges.HasIntersection( pCandidateParty->m_setSearchChallenges ); + } + + virtual bool BThreadedPartiesCompatible( const MatchParty_t *pLeftParty, const MatchParty_t *pRightParty ) const OVERRIDE + { +#ifdef USE_MVM_TOUR + if ( pLeftParty->m_iMannUpTourOfDuty != pRightParty->m_iMannUpTourOfDuty ) + return false; +#endif // USE_MVM_TOUR + + return !pLeftParty->m_setSearchChallenges.HasIntersection( pRightParty->m_setSearchChallenges ); + } + + // Intersect MvM challenges + bool BThreadedIntersectMatchWithParty( MatchDescription_t* pMatch, const MatchParty_t* pParty ) const OVERRIDE + { + // Check for blacklisted former members that tank compatibility + if ( !BCheckLeftMatchMembersAgainstParty( pMatch, pParty, &BThreadedFormerMemberMayRejoinJoinCasualOrMvMMatch) ) + { return false; } + + pMatch->m_setAcceptableChallenges.Intersect( pParty->m_setSearchChallenges ); + if ( pMatch->m_setAcceptableChallenges.IsEmpty() ) + return false; + +#ifdef USE_MVM_TOUR + if ( pMatch->m_eMatchGroup == k_nMatchGroup_MvM_MannUp ) + { + Assert( pParty->m_iMannUpTourOfDuty >= 0 ); + if ( pMatch->m_iMannUpTourOfDuty != pParty->m_iMannUpTourOfDuty ) + return false; + } + else + { + Assert( pParty->m_iMannUpTourOfDuty == k_iMvmTourIndex_NotMannedUp ); + } +#endif // USE_MVM_TOUR + + return true; + } + + virtual void GetServerDetails( const CMsgGameServerMatchmakingStatus& msg, int& nChallengeIndex, const char* pszMap ) const OVERRIDE + { + if ( msg.matchmaking_state() == ServerMatchmakingState_EMPTY ) // if we're empty, we can switch the challenge, so the current value doesn't matter + { + pszMap = ""; + } + else + { + nChallengeIndex = GetItemSchema()->FindMvmMissionByName( pszMap ); + } + } + + virtual const char* GetUnauthorizedPartyReason( CTFParty* pParty ) const OVERRIDE + { + pParty->CheckRemoveInvalidSearchChallenges(); + CMvMMissionSet searchChallenges; + pParty->GetSearchChallenges( searchChallenges ); + if ( searchChallenges.IsEmpty() ) + { + return "They want to play MvM, but set of search challenges is empty."; + } + + if ( pParty->GetMatchGroup() == k_nMatchGroup_MvM_MannUp ) + { + TFPartyManager()->YldUpdatePartyMemberData( pParty ); + if ( pParty->BAnyMemberWithoutTicket() ) + { + return "They want to play MannUp, but somebody doesn't have a ticket."; + } + +#ifdef USE_MVM_TOUR + // Make sure we know what tour of duty the want to play + if ( pParty->GetSearchMannUpTourIndex() < 0 ) + { + return "They want to play MannUp, but no tour of duty specified."; + } +#endif // USE_MVM_TOUR + } + + return NULL; + } + + virtual void Dump( const char *pszLeader, int nSpewLevel, int nLogLevel, const MatchParty_t* pMatch ) const OVERRIDE + { + CUtlString sSelectedPops; + int n = 0; + for ( int i = 0 ; i < GetItemSchema()->GetMvmMissions().Count() ; ++i ) + { + if ( pMatch->m_setSearchChallenges.GetMissionBySchemaIndex( i ) ) + { + if ( n > 0 ) + sSelectedPops += ", "; + if ( n >= 5 ) + { + sSelectedPops += "..."; + break; + } + sSelectedPops += GetItemSchema()->GetMvmMissionName( i ); + ++n; + } + } + EmitInfo( SPEW_GC, nSpewLevel, nLogLevel, "%s MvM Type: %s Search Pop: %s\n", pszLeader, + ( m_eMatchGroup == k_nMatchGroup_MvM_MannUp ? "Mann Up" : ( m_eMatchGroup == k_nMatchGroup_MvM_Practice ? "Bootcamp" : "UNKNOWN" ) ), + sSelectedPops.String() ); + EmitInfo( SPEW_GC, nSpewLevel, nLogLevel, "%s Best valve data center ping: %.0fms\n", pszLeader, pMatch->m_flPingClosestServer ); + } +#endif + +#ifdef CLIENT_DLL + virtual bool BGetRoundStartBannerParameters( int& nSkin, int& nBodyGroup ) const OVERRIDE + { + // Dont show in MvM...for now + return false; + } + + virtual bool BGetRoundDoorParameters( int& nSkin, int& nLogoBodyGroup ) const OVERRIDE + { + // Don't show in MvM...for now + return false; + } + + virtual const char *GetMapLoadBackgroundOverride( bool bWidescreen ) const OVERRIDE + { + if ( bWidescreen ) + { + return NULL; + } + + return "mvm_background_map"; + } +#endif + +#ifdef GAME_DLL + virtual bool InitServerSettingsForMatch( const CTFGSLobby* pLobby ) const OVERRIDE + { + bool bRet = IMatchGroupDescription::InitServerSettingsForMatch( pLobby ); + + if ( *pLobby->GetMissionName() != '\0' ) + { + TFGameRules()->SetNextMvMPopfile( pLobby->GetMissionName() ); + } + + return bRet; + } + + virtual void PostMatchClearServerSettings() const OVERRIDE + { + + } + + virtual void InitGameRulesSettings() const OVERRIDE + { + } + + virtual void InitGameRulesSettingsPostEntity() const OVERRIDE + { + } + + bool ShouldRequestLateJoin() const OVERRIDE + { + if ( !TFGameRules() || !TFGameRules()->IsMannVsMachineMode() ) + return false; + + // Check game state + switch ( TFGameRules()->State_Get() ) + { + case GR_STATE_INIT: + case GR_STATE_PREGAME: + case GR_STATE_STARTGAME: + case GR_STATE_PREROUND: + case GR_STATE_TEAM_WIN: + case GR_STATE_RESTART: + case GR_STATE_STALEMATE: + case GR_STATE_BONUS: + case GR_STATE_BETWEEN_RNDS: + return true; + + case GR_STATE_RND_RUNNING: + if ( TFObjectiveResource() && + !TFObjectiveResource()->GetMannVsMachineIsBetweenWaves() && + TFObjectiveResource()->GetMannVsMachineWaveCount() == TFObjectiveResource()->GetMannVsMachineMaxWaveCount() ) + { + int nMaxEnemyCountNoSupport = TFObjectiveResource()->GetMannVsMachineWaveEnemyCount(); + if ( nMaxEnemyCountNoSupport <= 0 ) + { + Assert( false ); // no enemies in wave?! + return false; + } + + // calculate number of remaining enemies + int nNumEnemyRemaining = 0; + + for ( int i = 0; i < MVM_CLASS_TYPES_PER_WAVE_MAX_NEW; ++i ) + { + int nClassCount = TFObjectiveResource()->GetMannVsMachineWaveClassCount( i ); + unsigned int iFlags = TFObjectiveResource()->GetMannVsMachineWaveClassFlags( i ); + + if ( iFlags & MVM_CLASS_FLAG_MINIBOSS ) + { + nNumEnemyRemaining += nClassCount; + } + + if ( iFlags & MVM_CLASS_FLAG_NORMAL ) + { + nNumEnemyRemaining += nClassCount; + } + } + + // if less then 40% of the last wave remains, lock people out from MM + if ( (float)nNumEnemyRemaining / (float)nMaxEnemyCountNoSupport < 0.4f ) + return false; + } + return true; + + case GR_STATE_GAME_OVER: + return false; + } + + Assert( false ); + return false; + } + + bool BMatchIsSafeToLeaveForPlayer( const CMatchInfo* pMatchInfo, const CMatchInfo::PlayerMatchData_t *pMatchPlayer ) const + { + bool bSafe = false; + // Allow safe leaving after you have played for N seconds or if the match drops below N players, even if it is + // still active. + int nAllowAfterSeconds = tf_mvm_allow_abandon_after_seconds.GetInt(); + int nAllowBelowPlayers = tf_mvm_allow_abandon_below_players.GetInt(); + RTime32 now = CRTime::RTime32TimeCur(); + bSafe = bSafe || ( nAllowAfterSeconds > 0 && (uint32)nAllowAfterSeconds < ( now - pMatchPlayer->rtJoinedMatch ) ); + bSafe = bSafe || ( nAllowBelowPlayers > 0 && pMatchInfo->GetNumActiveMatchPlayers() < nAllowBelowPlayers ); + + // Bootcamp is a magical nevar-abandon land + bSafe = bSafe || ( m_eMatchGroup == k_nMatchGroup_MvM_Practice ); + + return bSafe; + } + + virtual bool BPlayWinMusic( int nWinningTeam, bool bGameOver ) const OVERRIDE + { + // Not handled + return false; + } +#endif +}; + +class CLadderMatchGroupDescription : public IMatchGroupDescription +{ +public: + + class CLadderProgressionDesc : public IProgressionDesc + { + public: + CLadderProgressionDesc( EMatchGroup eMatchGroup ) + : IProgressionDesc( eMatchGroup + , "models/vgui/competitive_badge.mdl" + , "resource/ui/PvPCompRankPanel.res" + , "TF_Competitive_Rank" ) + { + // Bucket 1 + m_vecLevels.AddToTail( { 1, k_unDrilloRating_Ladder_Min, 11500, "competitive/competitive_badge_rank001", "#TF_Competitive_Rank_1", "MatchMaking.RankOneAchieved", "competitive/comp_background_tier001a" } ); + m_vecLevels.AddToTail( { 2, m_vecLevels.Tail().m_nEndXP, 13000, "competitive/competitive_badge_rank002", "#TF_Competitive_Rank_2", "MatchMaking.RankOneAchieved", "competitive/comp_background_tier001a" } ); + m_vecLevels.AddToTail( { 3, m_vecLevels.Tail().m_nEndXP, 14500, "competitive/competitive_badge_rank003", "#TF_Competitive_Rank_3", "MatchMaking.RankOneAchieved", "competitive/comp_background_tier001a" } ); + m_vecLevels.AddToTail( { 4, m_vecLevels.Tail().m_nEndXP, 16000, "competitive/competitive_badge_rank004", "#TF_Competitive_Rank_4", "MatchMaking.RankOneAchieved", "competitive/comp_background_tier002a" } ); + m_vecLevels.AddToTail( { 5, m_vecLevels.Tail().m_nEndXP, 17500, "competitive/competitive_badge_rank005", "#TF_Competitive_Rank_5", "MatchMaking.RankOneAchieved", "competitive/comp_background_tier002a" } ); + m_vecLevels.AddToTail( { 6, m_vecLevels.Tail().m_nEndXP, 19500, "competitive/competitive_badge_rank006", "#TF_Competitive_Rank_6", "MatchMaking.RankOneAchieved", "competitive/comp_background_tier002a" } ); + // Bucket 2 + m_vecLevels.AddToTail( { 7, m_vecLevels.Tail().m_nEndXP, 21500, "competitive/competitive_badge_rank007", "#TF_Competitive_Rank_7", "MatchMaking.RankTwoAchieved", "competitive/comp_background_tier003a" } ); + m_vecLevels.AddToTail( { 8, m_vecLevels.Tail().m_nEndXP, 23500, "competitive/competitive_badge_rank008", "#TF_Competitive_Rank_8", "MatchMaking.RankTwoAchieved", "competitive/comp_background_tier003a" } ); + m_vecLevels.AddToTail( { 9, m_vecLevels.Tail().m_nEndXP, 25500, "competitive/competitive_badge_rank009", "#TF_Competitive_Rank_9", "MatchMaking.RankTwoAchieved", "competitive/comp_background_tier003a" } ); + m_vecLevels.AddToTail( { 10, m_vecLevels.Tail().m_nEndXP, 28000, "competitive/competitive_badge_rank010", "#TF_Competitive_Rank_10", "MatchMaking.RankTwoAchieved", "competitive/comp_background_tier004a" } ); + m_vecLevels.AddToTail( { 11, m_vecLevels.Tail().m_nEndXP, 30500, "competitive/competitive_badge_rank011", "#TF_Competitive_Rank_11", "MatchMaking.RankTwoAchieved", "competitive/comp_background_tier004a" } ); + m_vecLevels.AddToTail( { 12, m_vecLevels.Tail().m_nEndXP, 33000, "competitive/competitive_badge_rank012", "#TF_Competitive_Rank_12", "MatchMaking.RankTwoAchieved", "competitive/comp_background_tier004a" } ); + // Bucket 3 + m_vecLevels.AddToTail( { 13, m_vecLevels.Tail().m_nEndXP, 35500, "competitive/competitive_badge_rank013", "#TF_Competitive_Rank_13", "MatchMaking.RankThreeAchieved", "competitive/comp_background_tier005a" } ); + m_vecLevels.AddToTail( { 14, m_vecLevels.Tail().m_nEndXP, 38000, "competitive/competitive_badge_rank014", "#TF_Competitive_Rank_14", "MatchMaking.RankThreeAchieved", "competitive/comp_background_tier005a" } ); + m_vecLevels.AddToTail( { 15, m_vecLevels.Tail().m_nEndXP, 40500, "competitive/competitive_badge_rank015", "#TF_Competitive_Rank_15", "MatchMaking.RankThreeAchieved", "competitive/comp_background_tier005a" } ); + m_vecLevels.AddToTail( { 16, m_vecLevels.Tail().m_nEndXP, 43500, "competitive/competitive_badge_rank016", "#TF_Competitive_Rank_16", "MatchMaking.RankThreeAchieved", "competitive/comp_background_tier006a" } ); + m_vecLevels.AddToTail( { 17, m_vecLevels.Tail().m_nEndXP, 46500, "competitive/competitive_badge_rank017", "#TF_Competitive_Rank_17", "MatchMaking.RankThreeAchieved", "competitive/comp_background_tier006a" } ); + // Bucket 4 + m_vecLevels.AddToTail( { 18, m_vecLevels.Tail().m_nEndXP, 50000, "competitive/competitive_badge_rank018", "#TF_Competitive_Rank_18", "MatchMaking.RankFourAchieved", "competitive/comp_background_tier006a" } ); + } + + const LevelInfo_t& GetLevelForExperience( uint32 nExperience ) const OVERRIDE + { + FixmeMMRatingBackendSwapping(); // Hard-coded drillo + + // The client may not have a rating yet, in which case they see 0 until they've been in a match. For level + // purposes, return minimum. + return IProgressionDesc::GetLevelForExperience( nExperience == 0 ? k_unDrilloRating_Ladder_Min : nExperience ); + } + +#ifdef CLIENT_DLL + virtual void SetupBadgePanel( CBaseModelPanel *pModelPanel, const LevelInfo_t& level ) const OVERRIDE + { + if ( !pModelPanel ) + return; + + int nLevelIndex = level.m_nLevelNum - 1; + int nSkin = nLevelIndex; + int nSkullsBodygroup = ( nLevelIndex % 6 ); + int nSparkleBodygroup = 0; + if ( level.m_nLevelNum == 18 ) nSparkleBodygroup = 1; + EnsureBadgePanelModel( pModelPanel ); + + int nBody = 0; + CStudioHdr studioHDR( pModelPanel->GetStudioHdr(), g_pMDLCache ); + + ::SetBodygroup( &studioHDR, nBody, ::FindBodygroupByName( &studioHDR, "skulls" ), nSkullsBodygroup ); + ::SetBodygroup( &studioHDR, nBody, ::FindBodygroupByName( &studioHDR, "sparkle" ), nSparkleBodygroup ); + + pModelPanel->SetBody( nBody ); + pModelPanel->SetSkin( nSkin ); + } + + virtual const uint32 GetLocalPlayerLastAckdExperience() const OVERRIDE + { + // This is bad and hard-coding a match group. We should just make XP a rating type and these functions + // should just say "use this rating for XP"/"use this rating for acked XP" + FixmeMMRatingBackendSwapping(); +#if defined STAGING_ONLY + if ( tf_test_pvp_rank_xp_change.GetInt() != -1 ) + { + return tf_test_pvp_rank_xp_change.GetInt(); + } +#endif + if ( !steamapicontext || !steamapicontext->SteamUser() ) + { + return 0u; + } + +#ifndef CLIENT_DLL + #error Make this call a yielding call if you are removing it from client ifdefs +#endif + CTFRatingData *pRating = CTFRatingData::YieldingGetPlayerRatingDataBySteamID( steamapicontext->SteamUser()->GetSteamID(), + k_nMMRating_6v6_DRILLO_PlayerAcknowledged ); + return pRating ? pRating->GetRatingData().unRatingPrimary : 0u; + } + + virtual const uint32 GetPlayerExperienceBySteamID( CSteamID steamid ) const OVERRIDE + { + // This is bad and hard-coding a match group. We should just make XP a rating type and these functions + // should just say "use this rating for XP"/"use this rating for acked XP" + FixmeMMRatingBackendSwapping(); +#if defined CLIENT_DLL && defined STAGING_ONLY + if ( tf_test_pvp_rank_xp_change.GetInt() != -1 ) + { + return tf_test_pvp_rank_xp_change.GetInt(); + } +#endif + +#ifndef CLIENT_DLL + #error Make this call a yielding call if you are removing it from client ifdefs +#endif + CTFRatingData *pRating = CTFRatingData::YieldingGetPlayerRatingDataBySteamID( steamapicontext->SteamUser()->GetSteamID(), + k_nMMRating_6v6_DRILLO ); + + return pRating ? pRating->GetRatingData().unRatingPrimary : 0u; + } +#endif // CLIENT_DLL + +#if defined GC + virtual bool BYldAcknowledgePlayerXPOnTransaction( CSQLAccess &transaction, + CTFSharedObjectCache *pLockedSOCache ) const OVERRIDE + { + // This is bad and a result of XP being just a rating in some places but a magic field elsewhere. + FixmeMMRatingBackendSwapping(); + + MMRatingData_t ratingData = pLockedSOCache->GetPlayerRatingData( k_nMMRating_6v6_DRILLO ); + MMRatingData_t ackedRatingData = pLockedSOCache->GetPlayerRatingData( k_nMMRating_6v6_DRILLO_PlayerAcknowledged ); + if ( ratingData == ackedRatingData ) + { return true; } + + // Feed it to acknowledged rating + return pLockedSOCache->BYieldingUpdatePlayerRating( transaction, + k_nMMRating_6v6_DRILLO_PlayerAcknowledged, + k_nMMRatingSource_PlayerAcknowledge, + 0, + ratingData ); + } + + virtual const bool BRankXPIsActuallyPrimaryMMRating() const OVERRIDE + { + // Gross hack due to XP being magically used differently in 6v6 right now. + FixmeMMRatingBackendSwapping(); + return true; + } +#endif // defined GC + +#if defined GC_DLL || ( defined STAGING_ONLY && defined CLIENT_DLL ) + virtual void DebugSpewLevels() const OVERRIDE + { + Msg( "Spewing comp levels:\n" ); + + // Walk the levels to find where the passed in experience value falls + for( int i=0; i< m_vecLevels.Count(); ++i ) + { + Msg( "Level %d:\t%d - %d. (+%d)\n", m_vecLevels[i].m_nLevelNum, m_vecLevels[i].m_nStartXP, m_vecLevels[i].m_nEndXP, ( m_vecLevels[i].m_nEndXP - m_vecLevels[i].m_nStartXP ) ); + } + } +#endif + }; + + CLadderMatchGroupDescription( EMatchGroup eMatchGroup, ConVar* pmm_match_group_size ) + : IMatchGroupDescription( eMatchGroup + , { eMatchMode_MatchMaker_LateJoinMatchBased // m_eLateJoinMode; + , eMMPenaltyPool_Ranked // m_ePenaltyPool + , true // m_bUsesSkillRatings; + , true // m_bSupportsLowPriorityQueue; + , true // m_bRequiresMatchID; + , LADDER_REQUIRED_SCORE // m_pmm_required_score; + , true // m_bUseMatchHud; + , "server_competitive.cfg" // m_pszExecFileName; + , pmm_match_group_size // m_pmm_match_group_size; + , NULL // m_pmm_match_group_size_minimum; + , MATCH_TYPE_COMPETITIVE // m_eMatchType; + , true // m_bShowPreRoundDoors; + , true // m_bShowPostRoundDoors; + , "#TF_Competitive_GameOver" // m_pszMatchEndKickWarning; + , "MatchMaking.RoundStart" // m_pszMatchStartSound; + , false // m_bAutoReady; + , true // m_bShowRankIcons; + , true // m_bUseMatchSummaryStage; + , true // m_bDistributePerformanceMedals; + , true // m_bIsCompetitiveMode; + , true // m_bUseFirstBlood; + , true // m_bUseReducedBonusTime; + , false // m_bUseAutoBalance; + , false // m_bAllowTeamChange; + , false // m_bRandomWeaponCrits; + , true // m_bFixedWeaponSpread; + , true // m_bRequireCompleteMatch; + , true // m_bTrustedServersOnly; + , true // m_bForceClientSettings; + , true // m_bAllowDrawingAtMatchSummary + , false // m_bAllowSpecModeChange + , false // m_bAutomaticallyRequeueAfterMatchEnds + , false // m_bUsesMapVoteOnRoundEnd + , false // m_bUsesXP + , true // m_bUsesDashboardOnRoundEnd + , true // m_bUsesSurveys + , true } ) // m_bStrictMatchmakerScoring + { + m_pProgressionDesc = new CLadderProgressionDesc( eMatchGroup ); + } + +#ifdef GC_DLL + virtual EMMRating PrimaryMMRatingBackend() const OVERRIDE + { + // Right now all ladders have this hard-coded. Live-swapping won't work either, since lobbies don't know what + // they were formed with in Match_Result + FixmeMMRatingBackendSwapping(); + return k_nMMRating_6v6_DRILLO; + } + + virtual const std::vector< EMMRating > &MatchResultRatingBackends() const OVERRIDE + { + FixmeMMRatingBackendSwapping(); // Shouldn't be hard-coded for 6v6, param + static std::vector< EMMRating > ladderRatings = { k_nMMRating_6v6_DRILLO, k_nMMRating_6v6_GLICKO }; + return ladderRatings; + } + + virtual bool InitMatchFromParty( MatchDescription_t* pMatch, const MatchParty_t* pParty ) const OVERRIDE { return true; } + virtual bool InitMatchFromLobby( MatchDescription_t* pMatch, CTFLobby* pLobby ) const OVERRIDE { return true; } + virtual void SyncMatchParty( const CTFParty *pParty, MatchParty_t *pMatchParty ) const OVERRIDE {}; + virtual void SelectModeSpecificParameters( const MatchDescription_t* pMatch, CTFLobby* pLobby ) const OVERRIDE + { + // Forced to use the competitive category + const SchemaGameCategory_t* pCategory = GetItemSchema()->GetGameCategory( kGameCategory_Competitive_6v6 ); + const char *pszMap = ( *tf_mm_ladder_force_map_by_name.GetString() ) ? tf_mm_ladder_force_map_by_name.GetString() : pCategory->GetRandomMap()->pszMapName; + pLobby->SetMapName( pszMap ); + } + + virtual void GetServerDetails( const CMsgGameServerMatchmakingStatus& msg, int& nChallengeIndex, const char* pszMap ) const OVERRIDE + {} + + virtual bool BThreadedPartiesCompatible( const MatchParty_t *pLeftParty, const MatchParty_t *pRightParty ) const OVERRIDE + { + return true; + } + + virtual bool BThreadedPartyCompatibleWithMatch( const MatchDescription_t* pMatch, const MatchParty_t *pCurrentParty ) const OVERRIDE + { + // Right now there's no criteria on ladder matches, but leavers are never allowed to rejoin + return BCheckLeftMatchMembersAgainstParty( pMatch, pCurrentParty, + [](const CTFMemcachedLobbyFormerMember& member) { return false; } ); + } + + virtual bool BThreadedIntersectMatchWithParty( MatchDescription_t* pMatch, const MatchParty_t* pParty ) const OVERRIDE + { + // No action on match, just check that they're compatible + return BThreadedPartyCompatibleWithMatch( pMatch, pParty ); + } + + virtual const char* GetUnauthorizedPartyReason( CTFParty* pParty ) const OVERRIDE + { + TFPartyManager()->YldUpdatePartyMemberData( pParty ); + if ( pParty->BAnyMemberWithoutCompetitiveAccess() ) + { + return "They want to play a competitive game, but somebody doesn't have competitive access"; + } + + return NULL; + } + + virtual void Dump( const char *pszLeader, int nSpewLevel, int nLogLevel, const MatchParty_t* pMatch ) const OVERRIDE + { + EmitInfo( SPEW_GC, nSpewLevel, nLogLevel, "%s Best Ladder ping: %.0fms\n", pszLeader, pMatch->m_flPingClosestServer ); + } +#endif + +#ifdef CLIENT_DLL + virtual bool BGetRoundStartBannerParameters( int& nSkin, int& nBodyGroup ) const OVERRIDE + { + // The comp skins start at skin 8 + nSkin = 8 + TFGameRules()->GetRoundsPlayed(); + nBodyGroup = 1; + return true; + } + + virtual bool BGetRoundDoorParameters( int& nSkin, int& nLogoBodyGroup ) const OVERRIDE + { + nLogoBodyGroup = 0; + nSkin = 0; + if( GTFGCClientSystem()->GetLobby() && + GTFGCClientSystem()->GetLobby()->Obj().average_rank() >= k_unDrilloRating_Ladder_HighSkill ) + { + // High skill has a different skin + nSkin = 1; + } + + return true; + } + virtual const char *GetMapLoadBackgroundOverride( bool bWidescreen ) const OVERRIDE + { + return ( bWidescreen ? "ranked_background_widescreen" : "ranked_background" ); + } +#endif + +#ifdef GAME_DLL + + virtual void PostMatchClearServerSettings() const OVERRIDE + { + Assert( TFGameRules() ); + TFGameRules()->EndCompetitiveMatch(); + } + + virtual void InitGameRulesSettings() const OVERRIDE + { + TFGameRules()->SetCompetitiveMode( true ); + TFGameRules()->SetAllowBetweenRounds( true ); + } + + virtual void InitGameRulesSettingsPostEntity() const OVERRIDE + { + CTeamControlPointMaster *pMaster = ( g_hControlPointMasters.Count() ) ? g_hControlPointMasters[0] : NULL; + bool bMultiStagePLR = ( tf_gamemode_payload.GetBool() && pMaster && pMaster->PlayingMiniRounds() && TFGameRules()->HasMultipleTrains() ); + bool bUseStopWatch = TFGameRules()->MatchmakingShouldUseStopwatchMode(); + bool bCTF = tf_gamemode_ctf.GetBool(); + bool bHighSkill = GTFGCClientSystem()->GetMatch() && GTFGCClientSystem()->GetMatch()->m_uAverageRank >= k_unDrilloRating_Ladder_HighSkill; + + // Exec our match settings + const char *pszExecFile = ( bHighSkill ) ? "server_competitive_rounds_win_conditions_high_skill.cfg" : "server_competitive_rounds_win_conditions.cfg"; + + if ( bUseStopWatch ) + { + pszExecFile = ( bHighSkill ) ? "server_competitive_stopwatch_win_conditions_high_skill.cfg" : "server_competitive_stopwatch_win_conditions.cfg"; + } + else if ( bMultiStagePLR || bCTF ) + { + pszExecFile = ( bHighSkill ) ? "server_competitive_max_rounds_win_conditions_high_skill.cfg" : "server_competitive_max_rounds_win_conditions.cfg"; + } + + engine->ServerCommand( CFmtStr( "exec %s\n", pszExecFile ) ); + + TFGameRules()->SetInStopWatch( bUseStopWatch ); + mp_tournament_stopwatch.SetValue( bUseStopWatch ); + } + + bool ShouldRequestLateJoin() const OVERRIDE + { + auto pTFGameRules = TFGameRules(); + if ( !pTFGameRules || !pTFGameRules->IsCompetitiveMode() || pTFGameRules->IsManagedMatchEnded() ) + { + return false; + } + + const CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch(); + + int nPlayers = pMatch->GetNumActiveMatchPlayers(); + int nMissingPlayers = pMatch->GetCanonicalMatchSize() - nPlayers; + + // Allow late-join if we're not started yet, have missing players, and have not lost everyone. + return nMissingPlayers && nPlayers && + ( pTFGameRules->State_Get() == GR_STATE_BETWEEN_RNDS || + pTFGameRules->State_Get() == GR_STATE_PREGAME || + pTFGameRules->State_Get() == GR_STATE_STARTGAME ); + } + + bool BMatchIsSafeToLeaveForPlayer( const CMatchInfo* pMatchInfo, const CMatchInfo::PlayerMatchData_t *pMatchPlayer ) const OVERRIDE + { + // It's only safe if the match is over + return pMatchInfo->BMatchTerminated(); + } + + virtual bool BPlayWinMusic( int nWinningTeam, bool bGameOver ) const OVERRIDE + { + if ( bGameOver ) + { + TFGameRules()->BroadcastSound( 255, ( nWinningTeam == TF_TEAM_RED ) ? "Announcer.CompMatchWinRed" : "Announcer.CompMatchWinBlu" ); + TFGameRules()->BroadcastSound( 255, ( nWinningTeam == TF_TEAM_RED ) ? "MatchMaking.MatchEndRedWinMusic" : "MatchMaking.MatchEndBlueWinMusic" ); + } + else + { + if ( nWinningTeam == TF_TEAM_RED ) + { + TFGameRules()->BroadcastSound( 255, "Announcer.CompRoundWinRed" ); + TFGameRules()->BroadcastSound( 255, "MatchMaking.RoundEndRedWinMusic" ); + } + else if ( nWinningTeam == TF_TEAM_BLUE ) + { + TFGameRules()->BroadcastSound( 255, "Announcer.CompRoundWinBlu" ); + TFGameRules()->BroadcastSound( 255, "MatchMaking.RoundEndBlueWinMusic" ); + } + else + { + TFGameRules()->BroadcastSound( 255, "Announcer.CompRoundStalemate" ); + TFGameRules()->BroadcastSound( 255, "MatchMaking.RoundEndStalemateMusic" ); + } + } + + return true; + } +#endif + +}; + +class CCasualMatchGroupDescription : public IMatchGroupDescription +{ +public: + + class CCasualProgressionDesc : public IProgressionDesc + { + public: + + CCasualProgressionDesc( EMatchGroup eMatchGroup ) + : IProgressionDesc( eMatchGroup + , "models/vgui/12v12_badge.mdl" + , "resource/ui/PvPCasualRankPanel.res" + , "TF_Competitive_Level" ) + , m_nLevelsPerStep( 25 ) + , m_nSteps( 6 ) + , m_nAverageXPPerGame( 500 ) + , m_nAverageMinutesPerGame( 30 ) + { + struct StepInfo_t + { + float m_flAvgGamerPerLevel; + const char* m_pszLevelUpSound; + }; + + const StepInfo_t stepInfo[] = { { 1.5f, "MatchMaking.LevelOneAchieved" } + , { 2.5f, "MatchMaking.LevelTwoAchieved" } + , { 4.f, "MatchMaking.LevelThreeAchieved" } + , { 6.f, "MatchMaking.LevelFourAchieved" } + , { 9.f, "MatchMaking.LevelFiveAchieved" } + , { 14.f, "MatchMaking.LevelSixAchieved" } }; + + uint32 nNumLevels = m_nLevelsPerStep * m_nSteps; + + uint32 nEndXPForLevel = 0; + for( uint32 i=0; i<nNumLevels; ++i ) + { + const uint32 nStep = i / m_nLevelsPerStep; + LevelInfo_t& level = m_vecLevels[ m_vecLevels.AddToTail() ]; + level.m_nLevelNum = i + 1; // This loop is 0-based, but users will start at level 1 + level.m_nStartXP = nEndXPForLevel; // Use the previous level's end as our start + + // We want the last level to have the same starting and ending XP value so the progress bar looks filled + // as soon as you hit max level. + if ( level.m_nLevelNum != nNumLevels ) + { + nEndXPForLevel += stepInfo[ nStep ].m_flAvgGamerPerLevel * m_nAverageXPPerGame; + } + + level.m_nEndXP = nEndXPForLevel; + level.m_pszLevelTitle = NULL; // Casual levels dont have titles + level.m_pszLevelUpSound = stepInfo[ nStep ].m_pszLevelUpSound; + level.m_pszLobbyBackgroundImage = "competitive/12v12_background001"; // All the same in casual + } + } + +#ifdef CLIENT_DLL + virtual void SetupBadgePanel( CBaseModelPanel *pModelPanel, const LevelInfo_t& level ) const OVERRIDE + { + if ( !pModelPanel ) + return; + + int nLevelIndex = level.m_nLevelNum - 1; + int nSkin = nLevelIndex / m_nLevelsPerStep; + int nStarsBodyGroup = ( ( nLevelIndex ) % 5 ) + 1; + int nBulletsBodyGroup = 0; + int nPlatesBodyGroup = 0; + int nBannerBodyGroup = 0; + + switch( ( ( nLevelIndex ) / 5 ) % 5 ) + { + case 0: + nBulletsBodyGroup = 0; + nPlatesBodyGroup = 0; + nBannerBodyGroup = 0; + break; + case 1: + nBulletsBodyGroup = 1; + nPlatesBodyGroup = 0; + nBannerBodyGroup = 0; + break; + case 2: + nBulletsBodyGroup = 2; + nPlatesBodyGroup = 1; + nBannerBodyGroup = 0; + break; + case 3: + nBulletsBodyGroup = 3; + nPlatesBodyGroup = 2; + nBannerBodyGroup = 1; + break; + case 4: + nBulletsBodyGroup = 4; + nPlatesBodyGroup = 3; + nBannerBodyGroup = 1; + break; + } + + EnsureBadgePanelModel( pModelPanel ); + + int nBody = 0; + CStudioHdr studioHDR( pModelPanel->GetStudioHdr(), g_pMDLCache ); + + ::SetBodygroup( &studioHDR, nBody, ::FindBodygroupByName( &studioHDR, "bullets" ), nBulletsBodyGroup ); + ::SetBodygroup( &studioHDR, nBody, ::FindBodygroupByName( &studioHDR, "plates" ), nPlatesBodyGroup ); + ::SetBodygroup( &studioHDR, nBody, ::FindBodygroupByName( &studioHDR, "banner" ), nBannerBodyGroup ); + ::SetBodygroup( &studioHDR, nBody, ::FindBodygroupByName( &studioHDR, "stars" ), nStarsBodyGroup ); + + pModelPanel->SetBody( nBody ); + pModelPanel->SetSkin( nSkin ); + } + + virtual const uint32 GetLocalPlayerLastAckdExperience() const OVERRIDE + { + // This is bad and hard-coding a match group. We should just make XP a rating type and these functions + // should just say "use this rating for XP"/"use this rating for acked XP" + FixmeMMRatingBackendSwapping(); +#if defined CLIENT_DLL && defined STAGING_ONLY + if ( tf_test_pvp_rank_xp_change.GetInt() != -1 ) + { + return tf_test_pvp_rank_xp_change.GetInt(); + } +#endif + + CSOTFLadderData *pLadderData = GetLocalPlayerLadderData( k_nMatchGroup_Casual_12v12 ); + if ( pLadderData ) + { + return pLadderData->Obj().last_ackd_experience(); + } + + return 0u; + } + + virtual const uint32 GetPlayerExperienceBySteamID( CSteamID steamid ) const OVERRIDE + { + // This is bad and hard-coding a match group. We should just make XP a rating type and these functions + // should just say "use this rating for XP"/"use this rating for acked XP" + FixmeMMRatingBackendSwapping(); +#if defined CLIENT_DLL && defined STAGING_ONLY + if ( tf_test_pvp_rank_xp_change.GetInt() != -1 ) + { + return tf_test_pvp_rank_xp_change.GetInt(); + } +#endif + +#ifndef CLIENT_DLL +#error This function is only okay on the client due to calling yielding stuff +#endif + CSOTFLadderData *pLadderData = YieldingGetPlayerLadderDataBySteamID( steamid, k_nMatchGroup_Casual_12v12 ); + if ( pLadderData ) + { + return pLadderData->Obj().experience(); + } + + return 0u; + } +#endif // CLIENT_DLL + +#if defined GC + virtual bool BYldAcknowledgePlayerXPOnTransaction( CSQLAccess &transaction, + CTFSharedObjectCache *pLockedSOCache ) const OVERRIDE + { + // This is bad and a result of XP being just a rating in some places but a magic field elsewhere. + FixmeMMRatingBackendSwapping(); + Assert( GGCTF()->IsSteamIDLockedByCurJob( pLockedSOCache->GetOwner() ) ); + + CSOTFLadderData *pLadderData = NULL; + + // Find their ladder data + CSharedObjectTypeCache *pItemTypeCache = pLockedSOCache->FindTypeCache( CSOTFLadderData::k_nTypeID ); + if ( pItemTypeCache ) + { + CSOTFLadderData queryItem( pLockedSOCache->GetOwner().GetAccountID(), k_nMatchGroup_Casual_12v12 ); + pLadderData = pLockedSOCache->FindTypedSharedObject< CSOTFLadderData >( queryItem ); + } + + if ( !pLadderData ) + { + return false; + } + + // Update the last ack'd to the current + // + // XXX(JohnS): This function needs to be destroyed, but if kept, we would probably want to fix + // CSharedObjectTransactionEx to work properly with multiple SOCaches, such that + // BYieldingUpdatePlayerRating could work with it, such that this function could just require a + // sharedobject transaction... + CSOTFLadderData dataCopy; + dataCopy.Copy( *pLadderData ); + uint32_t nAck = pLadderData->Obj().experience(); + dataCopy.Obj().set_last_ackd_experience( nAck ); + CUtlVector<int> dirtyFields( 0, 1 ); + dirtyFields.AddToTail( CSOTFLadderPlayerStats::kLastAckdExperienceFieldNumber ); + bool bRet = dataCopy.BYieldingAddWriteToTransaction( transaction, dirtyFields ); + if ( bRet ) + { + transaction.AddCommitListener( [pLockedSOCache, nAck, pLadderData]() { + Assert( GGCTF()->IsSteamIDLockedByCurJob( pLockedSOCache->GetOwner() ) ); + pLadderData->Obj().set_last_ackd_experience( nAck ); + pLockedSOCache->DirtyNetworkObject( pLadderData ); + }); + } + return bRet; + } + + virtual const bool BRankXPIsActuallyPrimaryMMRating() const OVERRIDE { return false; } +#endif // defined GC + +#if defined GC_DLL || ( defined STAGING_ONLY && defined CLIENT_DLL ) + virtual void DebugSpewLevels() const OVERRIDE + { + Msg( "Spewing casual levels:\n" ); + Msg( "Assuming average %d XP per game and average %d minutes per game\n", m_nAverageXPPerGame, m_nAverageMinutesPerGame ); + + uint32 nNumGamesToAchieve = 0; + // Walk the levels to find where the passed in experience value falls + for( int i=0; i< m_vecLevels.Count(); ++i ) + { + nNumGamesToAchieve += ( m_vecLevels[ i ].m_nEndXP - m_vecLevels[ i ].m_nStartXP ) / m_nAverageXPPerGame; + uint32 nExpectedMinutesToAchieve = nNumGamesToAchieve * m_nAverageMinutesPerGame; +#ifdef CLIENT_DLL + int nStep = i / m_nLevelsPerStep; + const CEconItemRarityDefinition* pRarity = GetItemSchema()->GetRarityDefinition( nStep + 1 ); + vgui::HScheme scheme = vgui::scheme()->GetScheme( "ClientScheme" ); + vgui::IScheme *pScheme = vgui::scheme()->GetIScheme( scheme ); + Color color = pScheme->GetColor( GetColorNameForAttribColor( pRarity->GetAttribColor() ), Color( 255, 255, 255, 255 ) ); + + ConColorMsg( color, "Level %d:\t%d - %d. Expected games required: %d Expected hours required: %.2f\n", m_vecLevels[ i ].m_nLevelNum, m_vecLevels[ i ].m_nStartXP, m_vecLevels[ i ].m_nEndXP, nNumGamesToAchieve, ( nExpectedMinutesToAchieve / 60.f ) ); +#else + DevMsg( "Level %d:\t%d - %d. Expected games required: %d Expected hours required: %.2f\n", m_vecLevels[ i ].m_nLevelNum, m_vecLevels[ i ].m_nStartXP, m_vecLevels[ i ].m_nEndXP, nNumGamesToAchieve, ( nExpectedMinutesToAchieve / 60.f ) ); +#endif + } + } +#endif + + private: + const uint32 m_nLevelsPerStep; + const uint32 m_nSteps; + const uint32 m_nAverageXPPerGame; + const uint32 m_nAverageMinutesPerGame; + }; + + CCasualMatchGroupDescription( EMatchGroup eMatchGroup, ConVar* pmm_match_group_size, ConVar* pmm_match_group_size_minimum ) + : IMatchGroupDescription( eMatchGroup + , { eMatchMode_MatchMaker_LateJoinMatchBased // m_eLateJoinMode; + , eMMPenaltyPool_Casual // m_ePenaltyPool + , true // m_bUsesSkillRatings; + , true // m_bSupportsLowPriorityQueue; + , true // m_bRequiresMatchID; + , CASUAL_REQUIRED_SCORE // m_pmm_required_score; + , true // m_bUseMatchHud; + , "server_casual.cfg" // m_pszExecFileName; + , pmm_match_group_size // m_pmm_match_group_size; + , pmm_match_group_size_minimum // m_pmm_match_group_size_minimum; + , MATCH_TYPE_CASUAL // m_eMatchType; + , true // m_bShowPreRoundDoors; + , true // m_bShowPostRoundDoors; + , "#TF_Competitive_GameOver" // m_pszMatchEndKickWarning; + , "MatchMaking.RoundStartCasual" // m_pszMatchStartSound; + , true // m_bAutoReady; + , false // m_bShowRankIcons; + , false // m_bUseMatchSummaryStage; + , false // m_bDistributePerformanceMedals; + , true // m_bIsCompetitiveMode; + , false // m_bUseFirstBlood; + , false // m_bUseReducedBonusTime; + , true // m_bUseAutoBalance; + , false // m_bAllowTeamChange; + , true // m_bRandomWeaponCrits; + , false // m_bFixedWeaponSpread; + , false // m_bRequireCompleteMatch; + , true // m_bTrustedServersOnly; + , false // m_bForceClientSettings; + , false // m_bAllowDrawingAtMatchSummary + , true // m_bAllowSpecModeChange + , true // m_bAutomaticallyRequeueAfterMatchEnds + , true // m_bUsesMapVoteOnRoundEnd + , true // m_bUsesXP + , true // m_bUsesDashboardOnRoundEnd + , true // m_bUsesSurveys + , false } ) // m_bStrictMatchmakerScoring + { + m_pProgressionDesc = new CCasualProgressionDesc( eMatchGroup ); + } + +#ifdef GC_DLL + virtual EMMRating PrimaryMMRatingBackend() const OVERRIDE + { + // Hard-coded for casual at the moment. Live-swapping won't work either, since lobbies don't know what they were + // formed with in Match_Result + FixmeMMRatingBackendSwapping(); + return k_nMMRating_12v12_DRILLO; + } + + virtual const std::vector< EMMRating > &MatchResultRatingBackends() const OVERRIDE + { + FixmeMMRatingBackendSwapping(); // Shouldn't be hard-coded for 12v12, param + static std::vector< EMMRating > casualRatings = { k_nMMRating_12v12_DRILLO, k_nMMRating_12v12_GLICKO }; + return casualRatings; + } + + // Copy party's casual criteria + virtual bool InitMatchFromParty( MatchDescription_t* pMatch, const MatchParty_t* pParty ) const OVERRIDE + { + pMatch->m_acceptableCasualCriteria.Clear(); + pMatch->m_acceptableCasualCriteria.CopyFrom( pParty->m_casualCriteria ); + + CCasualCriteriaHelper helper( pMatch->m_acceptableCasualCriteria ); + return helper.AnySelected(); + } + + virtual bool InitMatchFromLobby( MatchDescription_t* pMatch, CTFLobby* pLobby ) const OVERRIDE + { + pMatch->m_acceptableCasualCriteria.Clear(); + CCasualCriteriaHelper helper( pMatch->m_acceptableCasualCriteria ); + + const MapDef_t* pMap = GetItemSchema()->GetMasterMapDefByName( pLobby->GetMapName() ); + if ( pMap ) + { + helper.SetMapSelected( pMap->m_nDefIndex, true ); + } + + pMatch->m_acceptableCasualCriteria = helper.GetCasualCriteria(); + + return helper.AnySelected(); + } + + // Set the match party's criteria to the TFParty's criteria + virtual void SyncMatchParty( const CTFParty *pParty, MatchParty_t *pMatchParty ) const OVERRIDE + { + pMatchParty->m_casualCriteria.CopyFrom( pParty->Obj().search_casual() ); + } + + // Get all the valid categories and randomly choose a category to play + virtual void SelectModeSpecificParameters( const MatchDescription_t* pMatch, CTFLobby* pLobby ) const OVERRIDE + { + if ( *tf_mm_ladder_force_map_by_name.GetString() ) + { + pLobby->SetMapName( tf_mm_ladder_force_map_by_name.GetString() ); + return; + } + + CUtlVector< const MapDef_t* > vecValidMaps; + int nNumMaps = GetItemSchema()->GetMasterMapsList().Count(); + CCasualCriteriaHelper helper( pMatch->m_acceptableCasualCriteria ); + for( int i=0; i < nNumMaps; ++ i ) + { + const MapDef_t* pMapDef = GetItemSchema()->GetMasterMapsList()[ i ]; + + if ( helper.IsMapSelected( pMapDef ) ) + { + vecValidMaps.AddToTail( pMapDef ); + } + } + + if ( vecValidMaps.Count() == 0 ) + { + // We should have SOMETHING valid. + Assert( vecValidMaps.Count() > 0 ); + pLobby->SetMapName( "ctf_2fort" ); // Everybody loves 2fort + return; + } + + const char *pszMap = vecValidMaps[ RandomInt( 0, vecValidMaps.Count() - 1 ) ]->pszMapName; + pLobby->SetMapName( pszMap ); + } + + // Private helper to check an entire party's join permissions and criteria, for both PartyCompatible and Intersect + // below, since they do the same computation, one just keeps it. + bool BThreadedCheckPartyAllowedToJoinMatchAndIntersectCriteria( const MatchDescription_t* pMatch, + const MatchParty_t *pParty, + CCasualCriteriaHelper &criteria ) const + { + if ( *tf_mm_ladder_force_map_by_name.GetString() ) + { + // If a map is forced, we don't care about compatibility. We're obviously testing something. + return true; + } + + // Check for blacklisted former members that tank compatibility + if ( !BCheckLeftMatchMembersAgainstParty( pMatch, pParty, &BThreadedFormerMemberMayRejoinJoinCasualOrMvMMatch) ) + { return false; } + + // Intersect criteria with new party + criteria = CCasualCriteriaHelper( pMatch->m_acceptableCasualCriteria ); + criteria.Intersect( pParty->m_casualCriteria ); + + return criteria.AnySelected(); + } + + virtual bool BThreadedPartyCompatibleWithMatch( const MatchDescription_t* pMatch, const MatchParty_t *pCandidateParty ) const OVERRIDE + { + CCasualCriteriaHelper helper( pMatch->m_acceptableCasualCriteria ); + return BThreadedCheckPartyAllowedToJoinMatchAndIntersectCriteria( pMatch, pCandidateParty, helper ); + } + + virtual bool BThreadedIntersectMatchWithParty( MatchDescription_t* pMatch, const MatchParty_t* pParty ) const OVERRIDE + { + CCasualCriteriaHelper helper( pMatch->m_acceptableCasualCriteria ); + bool bRet = BThreadedCheckPartyAllowedToJoinMatchAndIntersectCriteria( pMatch, pParty, helper ); + + // Update the match + if ( bRet ) + { + pMatch->m_acceptableCasualCriteria = helper.GetCasualCriteria(); + return true; + } + + return false; + + } + + virtual bool BThreadedPartiesCompatible( const MatchParty_t *pLeftParty, const MatchParty_t *pRightParty ) const OVERRIDE + { + CCasualCriteriaHelper helper( pLeftParty->m_casualCriteria ); + helper.Intersect( pRightParty->m_casualCriteria ); + return helper.AnySelected(); + } + + virtual void GetServerDetails( const CMsgGameServerMatchmakingStatus& msg, int& nChallengeIndex, const char* pszMap ) const OVERRIDE + {} + + virtual const char* GetUnauthorizedPartyReason( CTFParty* pParty ) const OVERRIDE + { + // Don't need no credit card to ride this train + return NULL; + } + + virtual void Dump( const char *pszLeader, int nSpewLevel, int nLogLevel, const MatchParty_t* pMatch ) const OVERRIDE + { + CUtlString strSelectedMaps; + + CUtlVector< const MapDef_t* > vecValidMaps; + int nCount = 0; + int nNumMaps = GetItemSchema()->GetMasterMapsList().Count(); + CCasualCriteriaHelper helper( pMatch->m_casualCriteria ); + for( int i=0; i < nNumMaps; ++ i ) + { + const MapDef_t* pMapDef = GetItemSchema()->GetMasterMapsList()[ i ]; + if ( helper.IsMapSelected( pMapDef ) ) + { + if ( nCount > 0 ) + { + strSelectedMaps += ", "; + } + + strSelectedMaps += pMapDef->pszMapName; + ++nCount; + } + } + + EmitInfo( SPEW_GC, nSpewLevel, nLogLevel, "%s Search casual maps: %s\n", pszLeader, strSelectedMaps.String() ); + EmitInfo( SPEW_GC, nSpewLevel, nLogLevel, "%s Best casual server ping: %.0fms\n", pszLeader, pMatch->m_flPingClosestServer ); + } +#endif + +#ifdef CLIENT_DLL + virtual bool BGetRoundStartBannerParameters( int& nSkin, int& nBodyGroup ) const OVERRIDE + { + nBodyGroup = 0; + nSkin = TFGameRules()->GetRoundsPlayed(); + return true; + } + + virtual bool BGetRoundDoorParameters( int& nSkin, int& nLogoBodyGroup ) const OVERRIDE + { + nLogoBodyGroup = 1; + nSkin = 3; + return true; + } + + virtual const char *GetMapLoadBackgroundOverride( bool bWidescreen ) const OVERRIDE + { + return NULL; + } +#endif + +#ifdef GAME_DLL + virtual void PostMatchClearServerSettings() const OVERRIDE + { + Assert( TFGameRules() ); + TFGameRules()->MatchSummaryEnd(); + } + + virtual void InitGameRulesSettings() const OVERRIDE + { + TFGameRules()->SetAllowBetweenRounds( true ); + } + + virtual void InitGameRulesSettingsPostEntity() const OVERRIDE + { + CTeamControlPointMaster *pMaster = ( g_hControlPointMasters.Count() ) ? g_hControlPointMasters[0] : NULL; + bool bMultiStagePLR = ( tf_gamemode_payload.GetBool() && pMaster && pMaster->PlayingMiniRounds() && + pMaster->GetNumRounds() > 1 && TFGameRules()->HasMultipleTrains() ); + bool bCTF = tf_gamemode_ctf.GetBool(); + bool bUseStopWatch = TFGameRules()->MatchmakingShouldUseStopwatchMode(); + + // Exec our match settings + const char *pszExecFile = bUseStopWatch ? "server_casual_stopwatch_win_conditions.cfg" : + ( ( bMultiStagePLR || bCTF ) ? "server_casual_max_rounds_win_conditions.cfg" : "server_casual_rounds_win_conditions.cfg" ); + + if ( TFGameRules()->IsPowerupMode() ) + { + pszExecFile = "server_casual_max_rounds_win_conditions_mannpower.cfg"; + } + + engine->ServerCommand( CFmtStr( "exec %s\n", pszExecFile ) ); + + // leave stopwatch off for now + TFGameRules()->SetInStopWatch( false );//bUseStopWatch ); + mp_tournament_stopwatch.SetValue( false );//bUseStopWatch ); + } + + bool ShouldRequestLateJoin() const OVERRIDE + { + auto pTFGameRules = TFGameRules(); + if ( !pTFGameRules || !pTFGameRules->IsCompetitiveMode() || pTFGameRules->IsManagedMatchEnded() ) + { + Assert( false ); + return false; + } + + if ( pTFGameRules->BIsManagedMatchEndImminent() ) + return false; + + const CMatchInfo *pMatch = GTFGCClientSystem()->GetMatch(); + + int nPlayers = pMatch->GetNumActiveMatchPlayers(); + int nMissingPlayers = pMatch->GetCanonicalMatchSize() - nPlayers; + + // Allow late-join if we have missing players, have not lost everyone + return nMissingPlayers && nPlayers; + } + + bool BMatchIsSafeToLeaveForPlayer( const CMatchInfo* pMatchInfo, const CMatchInfo::PlayerMatchData_t *pMatchPlayer ) const + { + return true; + } + + virtual bool BPlayWinMusic( int nWinningTeam, bool bGameOver ) const OVERRIDE + { + // Custom for game over + if ( bGameOver ) + { + TFGameRules()->BroadcastSound( TF_TEAM_RED, nWinningTeam == TF_TEAM_RED ? "MatchMaking.MatchEndWinMusicCasual" : "MatchMaking.MatchEndLoseMusicCasual" ); + TFGameRules()->BroadcastSound( TF_TEAM_BLUE, nWinningTeam == TF_TEAM_BLUE ? "MatchMaking.MatchEndWinMusicCasual" : "MatchMaking.MatchEndLoseMusicCasual" ); + return true; + } + else + { + // Let non-match logic handle round wins + return false; + } + } +#endif +}; + +#if defined STAGING_ONLY && defined CLIENT_DLL +CON_COMMAND( spew_match_group_levels, "Spew all casual levels" ) +{ + if ( args.ArgC() < 2 ) + return; + + const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( (EMatchGroup)atoi( args[1] ) ); + if ( !pMatchDesc || !pMatchDesc->m_pProgressionDesc ) + return; + + pMatchDesc->m_pProgressionDesc->DebugSpewLevels(); +} +#endif + +const IMatchGroupDescription* GetMatchGroupDescription( const EMatchGroup& eGroup ) +{ + static CMvMMatchGroupDescription descBootcamp ( k_nMatchGroup_MvM_Practice, "server_bootcamp.cfg", /* bTrustedOnly */ false ); + static CMvMMatchGroupDescription descMannup ( k_nMatchGroup_MvM_MannUp, "server_mannup.cfg", /* bTrustedOnly */ true ); + static CLadderMatchGroupDescription descLadder6v6 ( k_nMatchGroup_Ladder_6v6, &tf_mm_match_size_ladder_6v6 ); + static CLadderMatchGroupDescription descLadder9v9 ( k_nMatchGroup_Ladder_9v9, &tf_mm_match_size_ladder_9v9 ); + static CLadderMatchGroupDescription descLadder12v12 ( k_nMatchGroup_Ladder_12v12, &tf_mm_match_size_ladder_12v12 ); + static CCasualMatchGroupDescription descCasual6v6 ( k_nMatchGroup_Casual_6v6, &tf_mm_match_size_ladder_6v6, NULL ); + static CCasualMatchGroupDescription descCasual9v9 ( k_nMatchGroup_Casual_9v9, &tf_mm_match_size_ladder_9v9, NULL ); + static CCasualMatchGroupDescription descCasual12v12 ( k_nMatchGroup_Casual_12v12, &tf_mm_match_size_ladder_12v12, \ + &tf_mm_match_size_ladder_12v12_minimum ); + + switch( eGroup ) + { + case k_nMatchGroup_MvM_Practice: + return &descBootcamp; + case k_nMatchGroup_MvM_MannUp: + return &descMannup; + + case k_nMatchGroup_Ladder_6v6: + return &descLadder6v6; + case k_nMatchGroup_Ladder_9v9: + return &descLadder9v9; + case k_nMatchGroup_Ladder_12v12: + return &descLadder12v12; + + case k_nMatchGroup_Casual_6v6: + return &descCasual6v6; + case k_nMatchGroup_Casual_9v9: + return &descCasual9v9; + case k_nMatchGroup_Casual_12v12: + return &descCasual12v12; + + default: +#ifdef GC_DLL + // We're going to expectedly hit this many times on the client/server. + // Only complain on the GC + Assert( false ); +#endif + break; + } + + return NULL; +} +// If you add a matchmaking group, handle it in the above switch +COMPILE_TIME_ASSERT( k_nMatchGroup_Casual_12v12 == ( k_nMatchGroup_Count - 1 ) ); |