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/client/tf/tf_quickplay_ui.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/client/tf/tf_quickplay_ui.cpp')
| -rw-r--r-- | game/client/tf/tf_quickplay_ui.cpp | 3005 |
1 files changed, 3005 insertions, 0 deletions
diff --git a/game/client/tf/tf_quickplay_ui.cpp b/game/client/tf/tf_quickplay_ui.cpp new file mode 100644 index 0000000..59ccd10 --- /dev/null +++ b/game/client/tf/tf_quickplay_ui.cpp @@ -0,0 +1,3005 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//============================================================================= +#include "cbase.h" + +// for messaging with the GC +#include "tf_gcmessages.h" +#include "tf_item_inventory.h" +#include "econ_game_account_server.h" +#include "gc_clientsystem.h" +#include "tf_quickplay.h" + +// ui related +#include "filesystem.h" +#include "tf_controls.h" +#include "clientmode_tf.h" +#include "confirm_dialog.h" +#include "econ_controls.h" +#include "game/client/iviewport.h" +#include "ienginevgui.h" +#include "tf_hud_mainmenuoverride.h" +#include "tf_hud_statpanel.h" +#include "tf_mouseforwardingpanel.h" +#include "vgui/ILocalize.h" +#include "vgui/ISurface.h" +#include "vgui_controls/PanelListPanel.h" +#include "vgui_controls/ProgressBar.h" +#include "vgui_controls/RadioButton.h" +#include "ServerBrowser/blacklisted_server_manager.h" +#include "rtime.h" + +#include "c_tf_gamestats.h" +#include "tf_gamerules.h" +#include "ServerBrowser/IServerBrowser.h" + +// other +#include "tier1/netadr.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include <tier0/memdbgon.h> + +const char *COM_GetModDirectory(); +extern int g_iQuickplaySessionIndex; + +#include "tf_matchmaking_scoring.h" + +ConVar tf_matchmaking_debug( "tf_matchmaking_spew_level", +#ifdef _DEBUG + "3", +#else + "1", +#endif +FCVAR_NONE, "Set to 1 for basic console spew of quickplay-related decisions. 4 for maximum verbosity." ); + +ConVar tf_quickplay_pref_community_servers( "tf_quickplay_pref_community_servers", "0", FCVAR_ARCHIVE, "0=Valve only, 1=Community only, 2=Either" ); + +#define TF_MATCHMAKING_SPEW( lvl, pStrFmt, ...) \ + if ( tf_matchmaking_debug.GetInt() >= lvl ) \ + { \ + Msg( pStrFmt, ##__VA_ARGS__ ); \ + } + +// Hack to force it to search for other app ID's for testing +static inline int GetTfMatchmakingAppID() +{ + // !TEST! + #ifdef STAGING_ONLY + ConVarRef sb_fake_app_id( "sb_fake_app_id" ); + if ( sb_fake_app_id.GetInt() != 0 ) + return sb_fake_app_id.GetInt(); + #endif + // return 440; + + return engine->GetAppID(); +} + +// A server that we recently were matched to and attempted to join +struct RecentlyMatchedServer +{ + uint32 m_ip; + uint16 m_port; + RTime32 m_timeMatched; +}; + +// Just store them in a simple list. This will be small, and it won't hurt +// to scan the whole thing, so it won't do any good to do anything fancy +static CUtlVector<RecentlyMatchedServer> s_vecRecentlyMatchedServers; + +// Search for a recently matched server. Also process cooldown expiry +static int FindRecentlyMatchedServer( uint32 ip, uint16 port ) +{ + RTime32 timeOfOldestEntryToKeep = CRTime::RTime32TimeCur() - tf_matchmaking_retry_cooldown_seconds.GetInt(); + int i = 0; + int result = -1; + while ( i < s_vecRecentlyMatchedServers.Count() ) + { + // Expire? + if ( s_vecRecentlyMatchedServers[i].m_timeMatched < timeOfOldestEntryToKeep ) + { + DevLog(" Expiring quickplay recently joined server entry %08X:%d\n", s_vecRecentlyMatchedServers[i].m_ip, (int)s_vecRecentlyMatchedServers[i].m_port ); + s_vecRecentlyMatchedServers.Remove( i ); + } + else + { + // Address match? + if ( s_vecRecentlyMatchedServers[i].m_ip == ip && s_vecRecentlyMatchedServers[i].m_port == port ) + { + Assert( result < 0 ); // dups in the list? + result = i; + } + + // Record is still active. Keep it + ++i; + } + } + + // Return index of the entry we found, if any + return result; +} + +ConVar tf_quickplay_pref_increased_maxplayers( "tf_quickplay_pref_increased_maxplayers", "0", FCVAR_ARCHIVE, "0=Default only, 1=Yes, 2=Don't care" ); +ConVar tf_quickplay_pref_disable_random_crits( "tf_quickplay_pref_disable_random_crits", "0", FCVAR_ARCHIVE, "0=Random crits enabled, 1=Random crits disabled, 2=Don't care" ); +ConVar tf_quickplay_pref_enable_damage_spread( "tf_quickplay_pref_enable_damage_spread", "0", FCVAR_ARCHIVE, "0=Damage spread disabled, 1=Damage spread enabled, 2=Don't care" ); +ConVar tf_quickplay_pref_respawn_times( "tf_quickplay_pref_respawn_times", "0", FCVAR_ARCHIVE, "0=Default respawn times only, 1=Instant respawn times ('norespawn' tag), 2=Don't care" ); +ConVar tf_quickplay_pref_advanced_view( "tf_quickplay_pref_advanced_view", "0", FCVAR_NONE, "0=Default to simplified view, 1=Default to more detailed options view" ); +ConVar tf_quickplay_pref_beta_content( "tf_quickplay_pref_beta_content", "0", FCVAR_ARCHIVE, "0=No beta content, 1=Only beta content" ); + + +static bool BHasTag( const CUtlStringList &TagList, const char *tag ) +{ + for ( int i = 0; i < TagList.Count(); i++ ) + { + if ( !Q_stricmp( TagList[i], tag) ) + { + return true; + } + } + return false; +} + +static void GetQuickplayTags( const QuickplaySearchOptions &opt, CUtlStringList &requiredTags, CUtlStringList &illegalTags ) +{ + // Always required + requiredTags.CopyAndAddToTail( "_registered" ); + + // Always illegal + illegalTags.CopyAndAddToTail( "friendlyfire" ); + illegalTags.CopyAndAddToTail( "highlander" ); + illegalTags.CopyAndAddToTail( "noquickplay" ); + illegalTags.CopyAndAddToTail( "trade" ); + + switch ( opt.m_eRespawnTimes ) + { + case QuickplaySearchOptions::eRespawnTimesDefault: + illegalTags.CopyAndAddToTail( "respawntimes" ); + illegalTags.CopyAndAddToTail( "norespawntime" ); + break; + + case QuickplaySearchOptions::eRespawnTimesInstant: + requiredTags.CopyAndAddToTail( "norespawntime" ); + break; + + default: + Assert( false ); + case QuickplaySearchOptions::eRespawnTimesDontCare: + break; + } + + switch ( opt.m_eRandomCrits ) + { + case QuickplaySearchOptions::eRandomCritsYes: + illegalTags.CopyAndAddToTail( "nocrits" ); + break; + + case QuickplaySearchOptions::eRandomCritsNo: + requiredTags.CopyAndAddToTail( "nocrits" ); + break; + + default: + Assert( false ); + case QuickplaySearchOptions::eRandomCritsDontCare: + break; + } + + switch ( opt.m_eDamageSpread ) + { + case QuickplaySearchOptions::eDamageSpreadYes: + requiredTags.CopyAndAddToTail( "dmgspread" ); + break; + + case QuickplaySearchOptions::eDamageSpreadNo: + illegalTags.CopyAndAddToTail( "dmgspread" ); + break; + + default: + Assert( false ); + case QuickplaySearchOptions::eDamageSpreadDontCare: + break; + } + + switch ( opt.m_eMaxPlayers ) + { + case QuickplaySearchOptions::eMaxPlayers24: + illegalTags.CopyAndAddToTail( "increased_maxplayers" ); + break; + + case QuickplaySearchOptions::eMaxPlayers30Plus: + requiredTags.CopyAndAddToTail( "increased_maxplayers" ); + break; + + default: + Assert( false ); + case QuickplaySearchOptions::eMaxPlayersDontCare: + break; + } + + switch ( opt.m_eBetaContent ) + { + case QuickplaySearchOptions::eBetaYes: + requiredTags.CopyAndAddToTail( "beta" ); + break; + + case QuickplaySearchOptions::eBetaNo: + illegalTags.CopyAndAddToTail( "beta" ); + break; + + default: + Assert( false ); + break; + } +} + +//----------------------------------------------------------------------------- + +static void OpenQuickplayDialogCallback( bool bConfirmed, void *pContext ) +{ + engine->ClientCmd_Unrestricted( "OpenQuickplayDialog" ); +} + +// Why does it take so many lines of code to do stuff like this? +static CDllDemandLoader g_ServerBrowser( "ServerBrowser" ); +static IServerBrowser *GetServerBrowser() +{ + static IServerBrowser *pServerBrowser = NULL; + if ( pServerBrowser == NULL ) + { + int iReturnCode; + pServerBrowser = (IServerBrowser *)g_ServerBrowser.GetFactory()( SERVERBROWSER_INTERFACE_VERSION, &iReturnCode ); + Assert( pServerBrowser ); + } + return pServerBrowser; +} + +//============================================================================= + +enum eGameServerFilter +{ + kGameServerListFilter_Internet, + kGameServerListFilter_Favorites, +}; + +struct sortable_gameserveritem_t +{ + sortable_gameserveritem_t() + { + m_bRegistered = false; + m_bValve = false; + m_bNewUserFriendly = false; + m_bMapIsQuickPlayOK = false; + userScore = -999.9f; + serverScore = -999.9f; + m_fRecentMatchPenalty = -999.9f; + m_fTotalScoreFromGC = -9999.9f; + m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_Invalid; + m_nOptionsScoreFromGC = INT_MAX; + } + + gameserveritem_t server; + bool m_bRegistered; + bool m_bValve; + bool m_bNewUserFriendly; + bool m_bMapIsQuickPlayOK; + float userScore; + float serverScore; + float m_fRecentMatchPenalty; + float m_fTotalScoreFromGC; + TF_Gamestats_QuickPlay_t::eServerStatus m_eStatus; + int m_nOptionsScoreFromGC; + + inline float TotalScore() const { return userScore + serverScore; } +}; + +// Store a server that has been scored by the GC and +// is ready to receive the final confirmation ping +struct gameserver_ping_queue_entry_t +{ + float m_fTotalScore; + netadr_t m_adr; +}; + +//----------------------------------------------------------------------------- +// Purpose: Score a game server based on number of players and ping +//----------------------------------------------------------------------------- +class CGameServerItemSort +{ +public: + bool Less( const sortable_gameserveritem_t *src1, const sortable_gameserveritem_t *src2, void *pCtx ) + { + // we want higher scores sorted to the front + return src1->TotalScore() > src2->TotalScore(); + } + bool Less( const gameserver_ping_queue_entry_t &src1, const gameserver_ping_queue_entry_t &src2, void *pCtx ) + { + // we want higher scores sorted to the front + return src1.m_fTotalScore > src2.m_fTotalScore; + } +}; + +// !KLUDGE! This is duplicated from serverbrowserQuickListPanel.h +class CQuickListPanel : public vgui::EditablePanel +{ + DECLARE_CLASS_SIMPLE( CQuickListPanel, vgui::EditablePanel ); + +public: + CQuickListPanel( vgui::Panel *parent, const char *panelName ); + + virtual void ApplySchemeSettings(vgui::IScheme *pScheme); + void SetMapName( const char *pMapName ); + void SetImage( const char *pMapName ); + void SetGameType( const char *pGameType ); + const char *GetMapName( void ) { return m_szMapName; } + void SetRefreshing( void ); + + virtual void OnMousePressed( vgui::MouseCode code ); + virtual void OnMouseDoublePressed( vgui::MouseCode code ); + void SetServerInfo ( KeyValues *pKV, int iListID, int iTotalServers ); + int GetListID( void ) { return m_iListID; } + + virtual void PerformLayout( void ) + { + BaseClass::PerformLayout(); + } + + MESSAGE_FUNC_INT( OnPanelSelected, "PanelSelected", state ) + { + if ( state ) + { + vgui::IScheme *pScheme = vgui::scheme()->GetIScheme( GetScheme() ); + + if ( pScheme && m_pBGroundPanel ) + { + m_pBGroundPanel->SetBgColor( pScheme->GetColor("QuickListBGSelected", Color(255, 255, 255, 0 ) ) ); + } + } + else + { + vgui::IScheme *pScheme = vgui::scheme()->GetIScheme( GetScheme() ); + + if ( pScheme && m_pBGroundPanel ) + { + m_pBGroundPanel->SetBgColor( pScheme->GetColor("QuickListBGDeselected", Color(255, 255, 255, 0 ) ) ); + } + } + + PostMessage( GetParent()->GetVParent(), new KeyValues("PanelSelected") ); + } + +private: + + char m_szMapName[64]; + + vgui::ImagePanel *m_pLatencyImage; + vgui::Label *m_pLatencyLabel; + vgui::Label *m_pPlayerCountLabel; + vgui::Label *m_pOtherServersLabel; + vgui::Label *m_pServerNameLabel; + vgui::Panel *m_pBGroundPanel; + vgui::ImagePanel *m_pMapImage; + + vgui::Panel *m_pListPanelParent; + vgui::Label *m_pGameTypeLabel; + vgui::Label *m_pMapNameLabel; + + vgui::ImagePanel *m_pReplayImage; + + int m_iListID; +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CQuickListPanel::CQuickListPanel( vgui::Panel* pParent, const char *pElementName ) : BaseClass( pParent, pElementName ) +{ + SetParent( pParent ); + + m_pListPanelParent = pParent; + + SetScheme( "SourceScheme" ); + SetProportional( false ); + + CMouseMessageForwardingPanel *panel = new CMouseMessageForwardingPanel(this, NULL); + panel->SetZPos(3); + + m_pLatencyImage = new ImagePanel( this, "latencyimage" ); + m_pPlayerCountLabel = new Label( this, "playercount", "" ); + m_pOtherServersLabel = new Label( this, "otherservercount", "" ); + m_pServerNameLabel = new Label( this, "servername", "" ); + m_pBGroundPanel = new Panel( this, "background" ); + m_pMapImage = new ImagePanel( this, "mapimage" ); + m_pGameTypeLabel = new Label( this, "gametype", "" ); + m_pMapNameLabel = new Label( this, "mapname", "" ); + m_pLatencyLabel = new Label( this, "latencytext", "" ); + m_pReplayImage = new ImagePanel( this, "replayimage" ); + + const char *pPathID = "PLATFORM"; + + if ( g_pFullFileSystem->FileExists( "Servers/QuickListPanel.res", "MOD" ) ) + { + pPathID = "MOD"; + } + + LoadControlSettings( "Servers/QuickListPanel.res", pPathID ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CQuickListPanel::ApplySchemeSettings(IScheme *pScheme) +{ + BaseClass::ApplySchemeSettings(pScheme); + + if ( pScheme && m_pBGroundPanel ) + { + m_pBGroundPanel->SetBgColor( pScheme->GetColor("QuickListBGDeselected", Color(255, 255, 255, 0 ) ) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CQuickListPanel::SetRefreshing( void ) +{ + if ( m_pServerNameLabel ) + { + m_pServerNameLabel->SetText( g_pVGuiLocalize->Find("#ServerBrowser_QuickListRefreshing") ); + } + + if ( m_pPlayerCountLabel ) + { + m_pPlayerCountLabel->SetVisible( false ); + } + if ( m_pOtherServersLabel ) + { + m_pOtherServersLabel->SetVisible( false ); + } + + if ( m_pLatencyImage ) + { + m_pLatencyImage->SetVisible( false ); + } + + if ( m_pReplayImage ) + { + m_pReplayImage->SetVisible( false ); + } + + if ( m_pLatencyLabel ) + { + m_pLatencyLabel->SetVisible( false ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CQuickListPanel::SetMapName( const char *pMapName ) +{ + Q_strncpy( m_szMapName, pMapName, sizeof( m_szMapName ) ); + + if ( m_pMapNameLabel ) + { + m_pMapNameLabel->SetText( pMapName ); + m_pMapNameLabel->SizeToContents(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CQuickListPanel::SetGameType( const char *pGameType ) +{ + if ( strlen ( pGameType ) == 0 ) + { + m_pGameTypeLabel->SetVisible( false ); + return; + } + + char gametype[ 512 ]; + Q_snprintf( gametype, sizeof( gametype ), "(%s)", pGameType ); + + m_pGameTypeLabel->SetText( gametype ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CQuickListPanel::SetServerInfo ( KeyValues *pKV, int iListID, int iTotalServers ) +{ + if ( pKV == NULL ) + return; + + m_iListID = iListID; + + m_pServerNameLabel->SetText( pKV->GetString( "name", " " ) ); + + int iPing = pKV->GetInt( "ping", 0 ); + + if ( iPing <= 100 ) + { + m_pLatencyImage->SetImage( "../vgui/icon_con_high.vmt" ); + } + else if ( iPing <= 150 ) + { + m_pLatencyImage->SetImage( "../vgui/icon_con_medium.vmt" ); + } + else + { + m_pLatencyImage->SetImage( "../vgui/icon_con_low.vmt" ); + } + + m_pLatencyImage->SetVisible( false ); + + //if ( GameSupportsReplay() ) + { + m_pReplayImage->SetVisible( pKV->GetInt( "Replay", 0 ) > 0 ); + } + + char ping[ 512 ]; + Q_snprintf( ping, sizeof( ping ), "%d ms", iPing ); + + m_pLatencyLabel->SetText( ping ); + m_pLatencyLabel->SetVisible( true ); + + wchar_t players[ 512 ]; + wchar_t playercount[16]; + wchar_t *pwszPlayers = g_pVGuiLocalize->Find("#ServerBrowser_Players"); + + g_pVGuiLocalize->ConvertANSIToUnicode( pKV->GetString( "players", " " ), playercount, sizeof( playercount ) ); + + _snwprintf( players, ARRAYSIZE( players ), L"%ls %ls", playercount, pwszPlayers ); + + m_pPlayerCountLabel->SetText( players ); + m_pPlayerCountLabel->SetVisible( true ); + + + // Now setup the other server count + if ( iTotalServers == 2 ) + { + m_pOtherServersLabel->SetText( g_pVGuiLocalize->Find("#ServerBrowser_QuickListOtherServer") ); + m_pOtherServersLabel->SetVisible( true ); + } + else if ( iTotalServers > 2 ) + { + wchar_t *pwszServers = g_pVGuiLocalize->Find("#ServerBrowser_QuickListOtherServers"); + _snwprintf( playercount, Q_ARRAYSIZE(playercount), L"%d", (iTotalServers-1) ); + g_pVGuiLocalize->ConstructString_safe( players, pwszServers, 1, playercount ); + m_pOtherServersLabel->SetText( players ); + m_pOtherServersLabel->SetVisible( true ); + } + else + { + m_pOtherServersLabel->SetVisible( false ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CQuickListPanel::SetImage( const char *pMapName ) +{ + char path[ 512 ]; + Q_snprintf( path, sizeof( path ), "materials/vgui/maps/menu_thumb_%s.vmt", pMapName ); + + char map[ 512 ]; + Q_snprintf( map, sizeof( map ), "maps/%s.bsp", pMapName ); + + if ( g_pFullFileSystem->FileExists( map, "MOD" ) == false ) + { + pMapName = "default_download"; + } + else + { + if ( g_pFullFileSystem->FileExists( path, "MOD" ) == false ) + { + pMapName = "default"; + } + } + + if ( m_pMapImage ) + { + char imagename[ 512 ]; + Q_snprintf( imagename, sizeof( imagename ), "..\\vgui\\maps\\menu_thumb_%s", pMapName ); + + m_pMapImage->SetImage ( imagename ); + m_pMapImage->SetMouseInputEnabled( false ); + } +} + +void CQuickListPanel::OnMousePressed( vgui::MouseCode code ) +{ + if ( m_pListPanelParent ) + { + vgui::PanelListPanel *pParent = dynamic_cast < vgui::PanelListPanel *> ( m_pListPanelParent ); + + if ( pParent ) + { + pParent->SetSelectedPanel( this ); + m_pListPanelParent->CallParentFunction( new KeyValues("ItemSelected", "itemID", -1 ) ); + } + + if ( code == MOUSE_RIGHT ) + { + m_pListPanelParent->CallParentFunction( new KeyValues("OpenContextMenu", "itemID", -1 ) ); + } + + } +} + +void CQuickListPanel::OnMouseDoublePressed( vgui::MouseCode code ) +{ + if ( code == MOUSE_RIGHT ) + return; + + // call the panel + OnMousePressed( code ); + + m_pListPanelParent->CallParentFunction( new KeyValues("ConnectToServer", "code", code) ); +} + +CUtlString GetCommaDelimited( const CUtlStringList &list ) +{ + CUtlString sResult; + FOR_EACH_VEC( list, idx ) + { + if ( idx != 0 ) + sResult.Append( "," ); + sResult.Append( list[idx] ); + } + return sResult; +} + +class CQuickplayWaitDialog; +static CQuickplayWaitDialog *s_pQuickPlayWaitingDialog; + +// Busy indicator and also server list if the didn't use "I'm feeling lucky" +class CQuickplayWaitDialog : public CGenericWaitingDialog, public ISteamMatchmakingServerListResponse, public ISteamMatchmakingPingResponse +{ + DECLARE_CLASS_SIMPLE( CQuickplayWaitDialog, CGenericWaitingDialog ); +public: + CQuickplayWaitDialog( bool bFeelingLucky, const QuickplaySearchOptions &opt ) + : CGenericWaitingDialog( NULL ) + , m_options( opt ) + , m_fHoursPlayed( 0 ) + , m_nAppID( GetTfMatchmakingAppID() ) + , m_hServerListRequest( NULL ) + , m_hServerQueryRequest( HSERVERQUERY_INVALID ) + , m_fPingA( tf_matchmaking_ping_a.GetFloat() ) + , m_fPingAScore( tf_matchmaking_ping_a_score.GetFloat() ) + , m_fPingB( tf_matchmaking_ping_b.GetFloat() ) + , m_fPingBScore( tf_matchmaking_ping_b_score.GetFloat() ) + , m_fPingC( tf_matchmaking_ping_c.GetFloat() ) + , m_fPingCScore( tf_matchmaking_ping_c_score.GetFloat() ) + , m_bFeelingLucky( bFeelingLucky ) + , m_eCurrentStep( k_EStep_GMSQuery ) + , m_timeGCScoreTimeout( 0.0 ) + , m_timeGMSSearchStarted( 0.0 ) + , m_timePingServerTimeout( 0.0 ) + , m_pBusyContainer( NULL ) + , m_pResultsContainer( NULL ) + , m_pServerListPanel( NULL ) + , m_pProgressBar( NULL ) + , m_ScoringNumbers( steamapicontext ? steamapicontext->SteamUser()->GetSteamID() : CSteamID() ) + { + + // Check the panel name since we are changing the resource + SetName("QuickPlayBusyDialog"); + + Assert( s_pQuickPlayWaitingDialog == NULL ); + s_pQuickPlayWaitingDialog = this; + + m_blackList.LoadServersFromFile( BLACKLIST_DEFAULT_SAVE_FILE, false ); + + m_fHoursPlayed = CTFStatPanel::GetTotalHoursPlayed(); + m_Stats.m_fUserHoursPlayed = m_fHoursPlayed; + m_Stats.m_iExperimentGroup = m_ScoringNumbers.m_eExperimentGroup; + + // Determine bonus we'll give to valve servers, using linear + // interpolation with clamped endpoints + float m_fValveBonusHrsA = tf_matchmaking_numbers_valve_bonus_hrs_a.GetFloat(); + float m_fValveBonusPtsA = tf_matchmaking_numbers_valve_bonus_pts_a.GetFloat(); + float m_fValveBonusHrsB = tf_matchmaking_numbers_valve_bonus_hrs_b.GetFloat(); + float m_fValveBonusPtsB = tf_matchmaking_numbers_valve_bonus_pts_b.GetFloat(); + Assert( m_fValveBonusHrsA < m_fValveBonusHrsB ); + if ( m_fHoursPlayed < m_fValveBonusHrsA ) + { + m_fValveBonus = m_fValveBonusPtsA; + } + else if ( m_fHoursPlayed < m_fValveBonusHrsB ) + { + m_fValveBonus = lerp( m_fValveBonusHrsA, m_fValveBonusPtsA, m_fValveBonusHrsB, m_fValveBonusPtsB, m_fHoursPlayed ); + } + else + { + m_fValveBonus = m_fValveBonusPtsB; + } + + // Calc bonus given if the map is noob friendly + float fNoobTime = tf_matchmaking_noob_hours_played.GetFloat(); + m_fNoobMapBonus = 0.0f; + if ( m_fHoursPlayed < fNoobTime ) + { + float fNoobPct = 1.0f - ( fNoobTime / fNoobTime ); + m_fNoobMapBonus = fNoobPct * tf_matchmaking_noob_map_score_boost.GetFloat(); + } + + switch ( m_options.m_eSelectedGameType ) + { + case kGameCategory_Quickplay: m_Stats.m_sUserGameMode = "(any)"; break; + case kGameCategory_Escort: m_Stats.m_sUserGameMode = "escort"; break; + case kGameCategory_CTF: m_Stats.m_sUserGameMode = "ctf"; break; + case kGameCategory_AttackDefense: m_Stats.m_sUserGameMode = "attackdefense"; break; + case kGameCategory_Koth: m_Stats.m_sUserGameMode = "koth"; break; + case kGameCategory_CP: m_Stats.m_sUserGameMode = "cp"; break; + case kGameCategory_EscortRace: m_Stats.m_sUserGameMode = "escortrace"; break; + case kGameCategory_EventMix: m_Stats.m_sUserGameMode = "eventmix"; break; + case kGameCategory_Event247: m_Stats.m_sUserGameMode = "event247"; break; + case kGameCategory_SD: m_Stats.m_sUserGameMode = "sd"; break; + case kGameCategory_RobotDestruction: m_Stats.m_sUserGameMode = "rd"; break; + case kGameCategory_Powerup: m_Stats.m_sUserGameMode = "powerup"; break; + case kGameCategory_Featured: m_Stats.m_sUserGameMode = "featured"; break; + case kGameCategory_Passtime: m_Stats.m_sUserGameMode = "passtime"; break; + case kGameCategory_Community_Update: m_Stats.m_sUserGameMode = "community_update"; break; + case kGameCategory_Misc: m_Stats.m_sUserGameMode = "misc"; break; + //case kQuickplayGameType_Arena: stats.m_sUserGameMode = "arena"; break; + //case kQuickplayGameType_Specialty: stats.m_sUserGameMode = "specialty"; break; + default: + Assert( false ); + m_Stats.m_sUserGameMode.Format( "%d", m_options.m_eSelectedGameType ); + break; + } + + SetDefLessFunc( m_mapServers ); + + // Sanity + Assert( 0.0 < m_fPingA ); Assert( m_fPingA < m_fPingB ); Assert( m_fPingB < m_fPingC ); + Assert( 1.0 > m_fPingAScore ); Assert( m_fPingAScore > m_fPingBScore ); Assert( m_fPingBScore > m_fPingCScore ); + + // Setup search filters + CUtlVector<MatchMakingKeyValuePair_t> vecServerFilters; + AddFilter( vecServerFilters, "gamedir", COM_GetModDirectory() ); + if ( GetUniverse() == k_EUniversePublic ) + { + AddFilter( vecServerFilters, "secure", "1" ); + AddFilter( vecServerFilters, "dedicated", "1" ); + } + AddFilter( vecServerFilters, "full", "1" ); // actually means "not full" + + GetQuickplayTags( m_options, m_vecStrRequiredTags, m_vecStrRejectTags ); + + // Specified map filter if one is specified + if ( !m_options.m_strMapName.IsEmpty() ) + { + AddFilter( vecServerFilters, "map", m_options.m_strMapName ); + } + else + { + // Add filter for the game mode tag + switch ( m_options.m_eSelectedGameType ) + { + case kGameCategory_Escort: + case kGameCategory_EscortRace: + m_vecStrRequiredTags.CopyAndAddToTail( "payload" ); + AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType ); + break; + case kGameCategory_Koth: + case kGameCategory_AttackDefense: + case kGameCategory_CP: + m_vecStrRequiredTags.CopyAndAddToTail( "cp" ); + AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType ); + break; + case kGameCategory_CTF: + m_vecStrRequiredTags.CopyAndAddToTail( "ctf" ); + AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType ); + break; + case kGameCategory_EventMix: + m_vecStrRequiredTags.CopyAndAddToTail( "eventmix" ); + m_vecStrRejectTags.CopyAndAddToTail( "event247" ); + AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType ); + break; + case kGameCategory_Event247: + m_vecStrRequiredTags.CopyAndAddToTail( "event247" ); + m_vecStrRejectTags.CopyAndAddToTail( "eventmix" ); + AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType ); + break; + case kGameCategory_SD: + m_vecStrRequiredTags.CopyAndAddToTail( "sd" ); + AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType ); + break; + case kGameCategory_RobotDestruction: + m_vecStrRequiredTags.CopyAndAddToTail( "rd" ); + AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType ); + break; + case kGameCategory_Powerup: + m_vecStrRequiredTags.CopyAndAddToTail( "powerup" ); + AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType ); + break; + case kGameCategory_Featured: + m_vecStrRequiredTags.CopyAndAddToTail( "featured" ); + AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType ); + break; + case kGameCategory_Passtime: + m_vecStrRequiredTags.CopyAndAddToTail( "passtime" ); + AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType ); + break; + case kGameCategory_Community_Update: + m_vecStrRequiredTags.CopyAndAddToTail( "community_update" ); + AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType ); + break; + case kGameCategory_Misc: + m_vecStrRequiredTags.CopyAndAddToTail( "misc" ); + AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType ); + break; + case kGameCategory_Quickplay: + // Grrrrr. Anything we can do here? + // The list of all the maps won't fit. We do not yet have an "or" syntax. + // So we're stuck filtering them client side + break; + default: + Assert( false ); + break; + } + } + + AddFilter( vecServerFilters, "ngametype", GetCommaDelimited( m_vecStrRejectTags ) ); + AddFilter( vecServerFilters, "gametype", GetCommaDelimited( m_vecStrRequiredTags ) ); + AddFilter( vecServerFilters, "steamblocking", "1" ); + + if ( m_options.m_eServers == QuickplaySearchOptions::eServersOfficial ) + { + AddFilter( vecServerFilters, "white", "1" ); + } + else if ( m_options.m_eServers == QuickplaySearchOptions::eServersCommunity ) + { + // community servers *ONLY* + AddFilter( vecServerFilters, "nor", "1" ); + AddFilter( vecServerFilters, "white", "1" ); + } + + // Remember when we started + m_timeGMSSearchStarted = Plat_FloatTime(); + + // Print for debugging + if ( tf_matchmaking_debug.GetInt() >= 2 ) + { + CUtlString sFilter; + FOR_EACH_VEC( vecServerFilters, idx ) + { + sFilter.Append( vecServerFilters[idx].m_szKey ); + sFilter.Append( " - " ); + sFilter.Append( vecServerFilters[idx].m_szValue ); + sFilter.Append( "\n" ); + } + Msg( "Using GMS filter: %s\n", sFilter.String() ); + } + + // Initiate the search + MatchMakingKeyValuePair_t *pFilters = vecServerFilters.Base(); // <<<< Note, this is weird, but correct. + m_hServerListRequest = steamapicontext->SteamMatchmakingServers()->RequestInternetServerList( GetTfMatchmakingAppID(), &pFilters, vecServerFilters.Count(), this ); + } + + virtual ~CQuickplayWaitDialog() + { + Assert( s_pQuickPlayWaitingDialog == this ); + s_pQuickPlayWaitingDialog = NULL; + + DestroyServerQueryRequest(); + DestroyServerListRequest(); + } + + virtual const char* GetResFile() const { return "Resource/UI/QuickPlayBusyDialog.res"; } + + void OnReceivedGCScores( CMsgTFQuickplay_ScoreServersResponse &msg ) + { + + // Make sure we're expecting them + if ( m_eCurrentStep != k_EStep_GCScore ) + { + Warning(" Received CGCTFQuickplay_ScoreServers_Response, but not expecting them (current step = %d)?\n", m_eCurrentStep ); + return; + } + + m_vecServerJoinQueue.RemoveAll(); + + // Do we have any GC scores? + if ( msg.servers_size() > 0 ) + { + TF_MATCHMAKING_SPEW( 1, "Received %d server scores from GC\n", msg.servers_size()); + + // Put them in a queue. We will try them in score order + TF_MATCHMAKING_SPEW( 2, "SteamID, IP, score\n" ); + for ( int i = 0; i < msg.servers_size(); ++i ) + { + const CMsgTFQuickplay_ScoreServersResponse_ServerInfo &info = msg.servers( i ); + netadr_t adr( info.server_address(), info.server_port() ); + CSteamID steamID( info.steam_id() ); + + TF_MATCHMAKING_SPEW( 2, "\"%s\", \"%s\", %.2f\n", + steamID.Render(), adr.ToString(), info.total_score() ); + + gameserver_ping_queue_entry_t item; + item.m_adr.SetIPAndPort( info.server_address(), info.server_port() ); + item.m_fTotalScore = info.total_score(); + int nOptionsScore = RemapValClamped( info.options_score(), 0.f, 30000.f, tf_mm_options_bonus.GetFloat(), tf_mm_options_penalty.GetFloat() ); + item.m_fTotalScore += nOptionsScore; + m_vecServerJoinQueue.InsertNoSort( item ); + + // Locate the original entry. Try by IP first, + // and if that fails, try steam ID. (Can happen due + // to mismatch of public/private IP) + int idx = m_mapServers.Find( adr ); + if ( m_mapServers.IsValidIndex( idx ) ) + { + // Might actually fail in race condition, but should + // be extremely rare. If it's happening frequently, + // we probably have a bug or have misunderstood something + Assert( m_mapServers[idx].server.m_steamID == steamID ); + } + else + { + bool bFound = false; + FOR_EACH_MAP_FAST( m_mapServers, j ) + { + if ( m_mapServers[j].server.m_steamID == steamID ) + { + Assert( !m_mapServers.IsValidIndex( idx ) ); // duplicate? + idx = j; + bFound = true; + break; + } + } + + if ( bFound ) + { + // Remove it and re-insert, updating the address + // we got from the GC, which is probably the public IP. + // this IP is more useful for scoring purposes + sortable_gameserveritem_t temp = m_mapServers[idx]; + temp.server.m_NetAdr.SetIP( info.server_address() ); + m_mapServers.RemoveAt( idx ); + idx = m_mapServers.InsertOrReplace( adr, temp ); + } + } + + if ( m_mapServers.IsValidIndex( idx ) ) + { + m_mapServers[ idx ].m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_Scored; + m_mapServers[ idx ].m_fTotalScoreFromGC = info.total_score(); + m_mapServers[ idx ].m_nOptionsScoreFromGC = info.options_score(); + } + else + { + Warning( "Received score from GC for %s @ %s, but I didn't ask for that server!\n", steamID.Render(), adr.ToString() ); + Assert( m_mapServers.IsValidIndex( idx ) ); + } + } + } else { + TF_MATCHMAKING_SPEW(1, "Using client-only scoring (no reputation data)\n" ); + + // Just add all of the servers that we thought were good enough to send to the GC + // to even request a score into the list of possible ones to join + FOR_EACH_MAP( m_mapServers, i ) + { + sortable_gameserveritem_t &s = m_mapServers[i]; + if ( s.m_eStatus >= TF_Gamestats_QuickPlay_t::k_Server_RequestedScore ) + { + gameserver_ping_queue_entry_t item; + item.m_adr.SetIPAndPort( s.server.m_NetAdr.GetIP(), s.server.m_NetAdr.GetConnectionPort() ); + item.m_fTotalScore = s.TotalScore(); // client-side score only, no reputation data + m_vecServerJoinQueue.InsertNoSort( item ); + } + } + } + m_vecServerJoinQueue.RedoSort(); + + if ( msg.servers_size() > 0 ) + { + TF_MATCHMAKING_SPEW(1, "Best GC score was %.2f\n", m_vecServerJoinQueue[0].m_fTotalScore ); + } + + // Show some UI + if ( !m_bFeelingLucky ) + { + ShowSelectServerUI(); + return; + } + + // OK, we have some candidates, but let's only try the first few. + // If they all fail, we probably have some bigger problem. + // !GAH! CUtlVector SetCount will not truncate an existing list, + // it will always fill the list with N dummy entries. I have + // no idea why. + while ( m_vecServerJoinQueue.Count() > 5 ) + { + m_vecServerJoinQueue.Remove( m_vecServerJoinQueue.Count()-1 ); + } + + // OK, we have a server. + C_CTFGameStats::ImmediateWriteInterfaceEvent( "response(quickplay)", "gc_server_found" ); + +// // !FIXME! Server pinging not working for some reason. +// // Just try to connect to the best one. +// ConnectToServer( m_vecServerJoinQueue[0].m_adr.GetIP(), m_vecServerJoinQueue[0].m_adr.GetPort() ); +// return; + + // Allright, begin loop pinging servers in order + // until we find one that is still OK to join. + // Hopefully and usually, the first one will + // still be available. But we sometimes waited + // too long and something happened on that server + // during the search and we need to go with our + // 2nd choice + m_eCurrentStep = k_EStep_PingCheckNotFull; + PingNextBestServer(); + } + + void ShowSelectServerUI() + { + m_eCurrentStep = k_EStep_SelectServerUI; + + if ( m_pBusyContainer ) + m_pBusyContainer->SetVisible( false ); + if ( m_pResultsContainer ) + m_pResultsContainer->SetVisible( true ); + m_adrCurrentPing.Clear(); + + if ( m_pServerListPanel ) + { + FOR_EACH_VEC( m_vecServerJoinQueue, idxServer ) + { + int idxMap = m_mapServers.Find( m_vecServerJoinQueue[ idxServer ].m_adr ); + if ( idxMap < 0 ) + continue; + sortable_gameserveritem_t &s = m_mapServers[ idxMap ]; + CQuickListPanel *pServerPanel = new CQuickListPanel( m_pServerListPanel, "QuickListPanel" ); + + char szFriendlyName[MAX_MAP_NAME]; + const char *pszFriendlyGameTypeName = GetServerBrowser()->GetMapFriendlyNameAndGameType( s.server.m_szMap, szFriendlyName, sizeof(szFriendlyName) ); + + pServerPanel->SetName( s.server.m_szMap ); + pServerPanel->SetMapName( GetMapDisplayName( s.server.m_szMap ) ); + pServerPanel->SetImage( s.server.m_szMap ); + pServerPanel->SetGameType( pszFriendlyGameTypeName ); + + KeyValues *pKV = new KeyValues( "ServerInfo" ); + + pKV->SetInt( "ping", s.server.m_nPing ); + pKV->SetString( "name", s.server.GetName() ); + pKV->SetString( "players", CFmtStr( "%d/%d", s.server.m_nPlayers, s.server.m_nMaxPlayers ) ); + pServerPanel->SetServerInfo( pKV, idxMap, 0 ); + pKV->deleteThis(); + pServerPanel->InvalidateLayout(); + pServerPanel->SetVisible( true ); + m_pServerListPanel->AddItem( NULL, pServerPanel ); + + // We need to reset the proportional flag again because the AddItem() call does a SetParent() + // call which copies the parent's proportional setting to the panel being added + pServerPanel->SetProportional( false ); + } + m_pServerListPanel->InvalidateLayout( false, true ); + } + } + + void ApplySchemeSettings( vgui::IScheme *pScheme ) + { + CGenericWaitingDialog::ApplySchemeSettings( pScheme ); + + m_pBusyContainer = dynamic_cast< vgui::EditablePanel* >( FindChildByName( "BusyContainer" ) ); + m_pResultsContainer = dynamic_cast< vgui::EditablePanel* >( FindChildByName( "ResultsContainer" ) ); + if ( m_pBusyContainer == NULL || m_pResultsContainer == NULL ) + { + Assert( m_pBusyContainer ); + Assert( m_pResultsContainer ); + return; + } + m_pBusyContainer->SetVisible( true ); + m_pResultsContainer->SetVisible( false ); + m_pServerListPanel = dynamic_cast< vgui::PanelListPanel* >( m_pResultsContainer->FindChildByName( "ServerList" ) ); Assert( m_pServerListPanel ); + if ( m_pServerListPanel ) + m_pServerListPanel->SetFirstColumnWidth( 0 ); + m_pProgressBar = dynamic_cast< vgui::ProgressBar* >( m_pBusyContainer->FindChildByName( "Progress" ) ); Assert( m_pProgressBar ); + + vgui::Label *pTitle = dynamic_cast< vgui::Label* >( m_pBusyContainer->FindChildByName( "TitleLabel" ) ); Assert( pTitle ); + if ( pTitle ) pTitle->SetText( m_bFeelingLucky ? "#TF_MM_WaitDialog_Title_FeelingLucky" : "#TF_MM_WaitDialog_Title_ShowServers" ); + + vgui::Panel *pPanel; + pPanel = m_pBusyContainer->FindChildByName( "CloseButton" ); Assert( pPanel ); + if ( pPanel ) pPanel->AddActionSignalTarget( this ); + pPanel = m_pResultsContainer->FindChildByName( "ConnectButton" ); Assert( pPanel ); + if ( pPanel ) pPanel->AddActionSignalTarget( this ); + pPanel = m_pResultsContainer->FindChildByName( "CancelButton" ); Assert( pPanel ); + if ( pPanel ) pPanel->AddActionSignalTarget( this ); + } + + void ConnectToServer( uint32 iAddress, uint16 iPort ) + { + + SendStats( TF_Gamestats_QuickPlay_t::k_Result_TriedToConnect ); + + netadr_t address( iAddress, iPort ); + Msg("Quickplay connecting to %s\n", address.ToString() ); + + // Close the window (and queue us for deletion), set our status + CloseMe(); + + // Find/add an entry in the list of recent servers + int iRecentIndex = FindRecentlyMatchedServer( iAddress, iPort ); + if ( iRecentIndex < 0 ) + { + iRecentIndex = s_vecRecentlyMatchedServers.AddToTail(); + s_vecRecentlyMatchedServers[ iRecentIndex ].m_ip = iAddress; + s_vecRecentlyMatchedServers[ iRecentIndex ].m_port = iPort; + } + + // Set time when we were matched to this server + s_vecRecentlyMatchedServers[ iRecentIndex ].m_timeMatched = CRTime::RTime32TimeCur(); + + const char *pszConnectCode = m_bFeelingLucky ? "quickplay" : "quickpick"; + char command[256]; + Q_snprintf( command, sizeof(command), "connect %s %s_%d\n", address.ToString(), pszConnectCode, g_iQuickplaySessionIndex ); + engine->ClientCmd_Unrestricted( command ); + } + + void OnCommand( const char *pCommand ) + { + if ( FStrEq( pCommand, "ConnectToServer" ) ) + { + UserConnectToServer(); + return; + } + BaseClass::OnCommand( pCommand ); + } + + virtual void OnUserClose() + { + SendStats( TF_Gamestats_QuickPlay_t::k_Result_UserCancel ); + BaseClass::OnUserClose(); + } + +protected: + + enum EStep + { + k_EStep_GMSQuery, // We're waiting on the GMS to give us back some servers + k_EStep_GCScore, // We're waiting on the GC to send us back scores + k_EStep_SelectServerUI, // User wasn't feeling lucky, wanted to see his options first + k_EStep_PingCheckNotFull, // We're looking for the highest scores server that isn't full + k_EStep_Terminated, // We've been terminated and are waiting to die + }; + EStep m_eCurrentStep; + bool m_bFeelingLucky; + + CUtlMap< netadr_t, sortable_gameserveritem_t > m_mapServers; + HServerListRequest m_hServerListRequest; + HServerQuery m_hServerQueryRequest; + uint32 m_nAppID; + CBlacklistedServerManager m_blackList; + QuickplaySearchOptions m_options; + CUtlStringList m_vecStrRequiredTags; + CUtlStringList m_vecStrRejectTags; + float m_fHoursPlayed; + double m_timeGCScoreTimeout; + double m_timeGMSSearchStarted; + double m_timePingServerTimeout; + netadr_t m_adrCurrentPing; + vgui::EditablePanel *m_pBusyContainer; + vgui::EditablePanel *m_pResultsContainer; + vgui::PanelListPanel *m_pServerListPanel; + vgui::ProgressBar *m_pProgressBar; + + // Piecewise linear data points for ping->score function + float m_fPingA, m_fPingAScore; + float m_fPingB, m_fPingBScore; + float m_fPingC, m_fPingCScore; + float m_fValveBonus; + float m_fNoobMapBonus; + + // List of scores servers we will try to join (after confirming they are still + // OK), in order + CUtlSortVector<gameserver_ping_queue_entry_t, CGameServerItemSort> m_vecServerJoinQueue; + + TF2ScoringNumbers_t m_ScoringNumbers; + + TF_Gamestats_QuickPlay_t m_Stats; + + void SendStats( TF_Gamestats_QuickPlay_t::eResult result ) + { + + // Set final result code + m_Stats.m_eResultCode = result; + + // Duration of total process + m_Stats.m_fSearchTime = Plat_FloatTime() - m_timeGMSSearchStarted; + + // Populate the server list + FOR_EACH_MAP( m_mapServers, i ) + { + sortable_gameserveritem_t &item = m_mapServers[i]; + TF_Gamestats_QuickPlay_t::Server_t &s = m_Stats.m_vecServers[m_Stats.m_vecServers.AddToTail()]; + Assert( item.m_eStatus != TF_Gamestats_QuickPlay_t::k_Server_Invalid ); + s.m_eStatus = item.m_eStatus; + s.m_ip = item.server.m_NetAdr.GetIP(); + s.m_port = item.server.m_NetAdr.GetConnectionPort(); + s.m_bSecure = item.server.m_bSecure; + s.m_bRegistered = item.m_bRegistered; + s.m_bValve = item.m_bValve; + s.m_nPlayers = item.server.m_nPlayers; + s.m_nMaxPlayers = item.server.m_nMaxPlayers; + s.m_sMapName = item.server.m_szMap; + s.m_sTags = item.server.m_szGameTags; + s.m_iPing = item.server.m_nPing; + s.m_bMapIsQuickPlayOK = s.m_bMapIsQuickPlayOK; + s.m_bMapIsNewUserFriendly = item.m_bNewUserFriendly; + s.m_fScoreClient = item.userScore; + s.m_fScoreServer = item.serverScore; + s.m_fScoreGC = item.m_fTotalScoreFromGC; + } + + // Send em + C_CTF_GameStats.QuickplayResults( m_Stats ); + } + +// +// ISteamMatchmakingServerListResponse overrides +// + + virtual void ServerResponded( HServerListRequest hRequest, int iServer ) + { + Assert( m_eCurrentStep == k_EStep_GMSQuery && m_hServerListRequest ); + gameserveritem_t *server = steamapicontext->SteamMatchmakingServers()->GetServerDetails( hRequest, iServer ); + if ( server ) + { + ServerResponded( *server ); + } + } + + virtual void ServerFailedToRespond( HServerListRequest hRequest, int iServer ) + { + // Should only happen during server list process, in which case we don't + // care. (We just won't add it to our list.) + Assert( m_eCurrentStep == k_EStep_GMSQuery && m_hServerListRequest ); + } + + virtual void RefreshComplete( HServerListRequest hRequest, EMatchMakingServerResponse response ) + { + // Destroy our request. We'll pick up on this fact next time in Think() + // and send the best servers to the GC to be scored + Assert( m_eCurrentStep == k_EStep_GMSQuery && m_hServerListRequest ); + DestroyServerListRequest(); + } + +// +// ISteamMatchmakingPingResponse overrides +// + + virtual void ServerResponded( gameserveritem_t &server ) + { + + // Ignore servers with bogus addres. Is this possible? + if ( !server.m_NetAdr.GetIP() || !server.m_NetAdr.GetConnectionPort() ) + { + Assert( server.m_NetAdr.GetIP() && server.m_NetAdr.GetConnectionPort() ); + return; + } + + // Bogus steam ID? + if ( !server.m_steamID.IsValid() || !server.m_steamID.BGameServerAccount() || server.m_steamID.GetAccountID() == 0 ) + { + TF_MATCHMAKING_SPEW( 2, "Quickplay ignoring gameserver at %s with invalid Steam ID %s", server.m_NetAdr.GetQueryAddressString(), server.m_steamID.Render() ); + return; + } + + // + // Filter the server + // + sortable_gameserveritem_t item; + item.server = server; + item.m_bNewUserFriendly = false; + // XXX(JohnS): This function is no longer available to clients with the matchmaking re-work. If we start using + // this UI for valve servers again we will need to fix this somehow. + // BIsIPInValveAddressSpace( item.server.m_NetAdr.GetIP() ); + item.m_bValve = false; + + // Parse out tags + CUtlStringList TagList; // auto-deletes strings on scope exit + if ( item.server.m_szGameTags && item.server.m_szGameTags[0] ) + { + V_SplitString( item.server.m_szGameTags, ",", TagList ); + } + + // Check if the server is registered + item.m_bRegistered = BHasTag( TagList, "_registered" ); + + const SchemaMap_t *pMapInfo = GetItemSchema()->GetMapForName( item.server.m_szMap ); + if ( pMapInfo != NULL ) + { + item.m_bMapIsQuickPlayOK = true; + item.m_bNewUserFriendly = pMapInfo->eQuickplayType == kQuickplay_AllUsers; + } + else + { + item.m_bMapIsQuickPlayOK = false; + item.m_bNewUserFriendly = false; + } + + // Joined recently? + CalculateRecentMatchPenalty( item ); + + // Do some basic filtering. We put an extremely low score in there if the server fails + // to match the criteria. The actual value isn't important here, but it's handy to have + // different values for different failure criteria for stats. + int failureCodes = 0; + if ( !PassesFilter( item.server ) ) failureCodes |= (1<<15); + if ( !HasAppropriateTags(TagList) ) failureCodes |= (1<<14); + if ( pMapInfo == NULL ) + failureCodes |= (1<<13); // unknown map + else + { + + // Map is known to us, make sure it matches search criteria + switch ( m_options.m_eSelectedGameType ) + { + case kGameCategory_EventMix: + if ( pMapInfo->eGameCategory != kGameCategory_EventMix && pMapInfo->eGameCategory != kGameCategory_Event247 ) + failureCodes |= (1<<12); + break; + + case kGameCategory_Quickplay: + // Any map that's in the list will do + break; + + default: + // Must match requested game mode + if ( pMapInfo->eGameCategory != m_options.m_eSelectedGameType ) + failureCodes |= (1<<12); + } + } + if ( server.m_nPlayers >= server.m_nMaxPlayers ) failureCodes |= (1<<11); + if ( m_blackList.IsServerBlacklisted( server ) ) failureCodes |= (1<<10); + + if ( failureCodes != 0 ) + { + item.serverScore = item.userScore = -(float)failureCodes; + item.m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_Ineligible; + + // Check for certain failures that a valve server should never have + if ( item.m_bValve && ( failureCodes & ( (1<<15) | (1<<14) | (1<<13) ) ) ) + { + Warning( "Valve-hosted server '%s' does not meet quickplay criteria?\n", server.GetName() ); + } + } + else + { + // Server pases basic filters and should be scored + item.m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_Eligible; + + // Start a score based only on the server, not on us + int nHumans = item.server.m_nPlayers - item.server.m_nBotPlayers; + int nNumInSearchParty = 1; // we are a party of 1 + item.serverScore = QuickplayCalculateServerScore( nHumans, item.server.m_nBotPlayers, item.server.m_nMaxPlayers, nNumInSearchParty ); + item.serverScore += 6.0f; // !KLUDGE! Add offset to keep score ranges comparable with historical norms. (We used to give bonuses for some things we now simply require.) + + // Start calculating score based on our own data + item.userScore = 0.0f; + + // Evaluate piecewise linear ping->score function + float fPing = (float)item.server.m_nPing; + float fPingScore = 0.0f; + if ( fPing < m_fPingA ) + { + fPingScore = lerp( 0.0f, 1.0f, m_fPingA, m_fPingAScore, fPing ); + } + else if ( fPing < m_fPingB ) + { + fPingScore = lerp( m_fPingA, m_fPingAScore, m_fPingB, m_fPingCScore, fPing ); + } + else + { + // note: This continues to get worse and worse if the ping > pingC + fPingScore = lerp( m_fPingB, m_fPingBScore, m_fPingC, m_fPingCScore, fPing ); + } + item.userScore += fPingScore; + + // Give a bonus to valve servers + if ( item.m_bValve ) + { + item.userScore += m_fValveBonus; + } + + // weight "newbie" servers a higher + if ( item.m_bNewUserFriendly ) + { + item.userScore += m_fNoobMapBonus; + } + + // Add in recently-joined cooldown factor + item.userScore -= item.m_fRecentMatchPenalty; + + // Penalty for rule changes + //item.userScore -= QuickplayCalculateRuleChangePenalty( TagList ); + + // + // Experiment + // + switch ( m_ScoringNumbers.m_eExperimentGroup ) + { + default: + Assert( !"Bogus experiment group" ); + case TF2ScoringNumbers_t::k_ExperimentGroup_Experiment1_Control: + case TF2ScoringNumbers_t::k_ExperimentGroup_Experiment1_ValveBiasInactive: + case TF2ScoringNumbers_t::k_ExperimentGroup_Experiment1_CommunityBiasInactive: + // No modifications + break; + + case TF2ScoringNumbers_t::k_ExperimentGroup_Experiment1_ValveBias: + + // Give valve servers with a good ping a significant bonus + item.userScore -= 1.0f; + if ( item.m_bValve ) + { + item.userScore += fPingScore; + } + break; + + case TF2ScoringNumbers_t::k_ExperimentGroup_Experiment1_CommunityBias: + + // Give community servers with a good ping a significant bonus + item.userScore -= 1.0f; + if ( !item.m_bValve ) + { + item.userScore += fPingScore; + } + break; + } + } + + netadr_t netAdr( server.m_NetAdr.GetIP(), server.m_NetAdr.GetConnectionPort() ); + + // Are we still in the initial pass, or are we pinging servers? + if ( m_eCurrentStep == k_EStep_GMSQuery ) + { + + // Insert/update our server map + m_mapServers.InsertOrReplace( netAdr, item ); + } + else if ( m_eCurrentStep == k_EStep_PingCheckNotFull || m_eCurrentStep == k_EStep_SelectServerUI ) + { + + // It had better be from who we expect + if ( netAdr.GetIPNetworkByteOrder() != m_adrCurrentPing.GetIPNetworkByteOrder() || netAdr.GetPort() != m_adrCurrentPing.GetPort() ) + { + // Of course, we could receive a packet on the network at any + // time. But we expect the steam API's to deal with that. + // But still, just in case.... + Warning( "Received unexpected server ping from %s\n", netAdr.ToString() ); + } + else + { + + // !TEST! Simulate server filling up + //failureCodes |= (1<<11); + + // Find it in the server list; update the result + int iServerIndex = m_mapServers.Find( m_adrCurrentPing ); + Assert( m_mapServers.IsValidIndex(iServerIndex) ); + + if ( failureCodes == 0 ) + { + if ( m_mapServers.IsValidIndex(iServerIndex) ) + { + Assert( m_mapServers[iServerIndex].m_eStatus == TF_Gamestats_QuickPlay_t::k_Server_Pinged ); + m_mapServers[iServerIndex].m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_Connected; + } + + // Still joinable. JOIN IT! + TF_MATCHMAKING_SPEW(2, "Reply ping from %s. Client score is now %.02f.\n", netAdr.ToString(), item.TotalScore() ); + ConnectToServer( netAdr.GetIPHostByteOrder(), netAdr.GetPort() ); + } + else + { + if ( m_mapServers.IsValidIndex(iServerIndex) ) + { + Assert( m_mapServers[iServerIndex].m_eStatus == TF_Gamestats_QuickPlay_t::k_Server_Pinged ); + m_mapServers[iServerIndex].m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_PingIneligible; + } + + // We missed the window of opportunity. + // Move on to our next best choice + TF_MATCHMAKING_SPEW(2, "Reply ping from %s, but no longer joinable!\n", netAdr.ToString() ); + if ( m_eCurrentStep == k_EStep_SelectServerUI ) + { + if ( failureCodes & (1<<11) ) + QuickpickPingFailed( "#TF_Quickplay_SelectedServer_Full" ); + else + QuickpickPingFailed( "#TF_Quickplay_SelectedServer_NoLongerMeetsCriteria" ); + } + else + { + PingNextBestServer(); + } + } + } + } + else + { + // Eh? + Assert( false ); + } + } + + virtual void ServerFailedToRespond() + { + if ( m_eCurrentStep == k_EStep_PingCheckNotFull || m_eCurrentStep == k_EStep_SelectServerUI ) + { + TF_MATCHMAKING_SPEW(2, "Timeout waiting on ping reply from %s.\n", m_adrCurrentPing.ToString() ); + + // Find it in the server list; record the failure result + int iServerIndex = m_mapServers.Find( m_adrCurrentPing ); + if ( m_mapServers.IsValidIndex(iServerIndex) ) + { + Assert( m_mapServers[iServerIndex].m_eStatus == TF_Gamestats_QuickPlay_t::k_Server_Pinged ); + m_mapServers[iServerIndex].m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_PingTimedOut; + } + else + { + // Wat. + Assert( m_mapServers.IsValidIndex(iServerIndex) ); + } + + if ( m_eCurrentStep == k_EStep_SelectServerUI ) + { + QuickpickPingFailed( "#TF_Quickplay_SelectedServer_Timeout" ); + } + else + { + PingNextBestServer(); + } + } + else + { + // We can get some timeouts after we've finished, during cleanup. + // Just ignore it + Assert( m_eCurrentStep == k_EStep_Terminated ); + } + } + +// +// Internal members +// + bool HasAppropriateTags( const CUtlStringList &TagList ) + { + FOR_EACH_VEC( m_vecStrRequiredTags, idx ) + { + if ( !BHasTag( TagList, m_vecStrRequiredTags[idx] ) ) + return false; + } + FOR_EACH_VEC( m_vecStrRejectTags, idx ) + { + if ( BHasTag( TagList, m_vecStrRejectTags[idx] ) ) + return false; + } + return true; + } + + void DestroyServerListRequest() + { + if ( m_hServerListRequest ) + { + steamapicontext->SteamMatchmakingServers()->ReleaseRequest( m_hServerListRequest ); + m_hServerListRequest = NULL; + } + } + + void DestroyServerQueryRequest() + { + if ( m_hServerQueryRequest != HSERVERQUERY_INVALID ) + { + steamapicontext->SteamMatchmakingServers()->CancelServerQuery( m_hServerQueryRequest ); + m_hServerQueryRequest = HSERVERQUERY_INVALID; + } + } + + static void AddFilter( CUtlVector<MatchMakingKeyValuePair_t> &vecServerFilters, const char *pchKey, const char *pchValue ) + { + // @note Tom Bui: this is to get around the linker error where we don't like strncpy in MatchMakingKeyValuePair_t's constructor! + int idx = vecServerFilters.AddToTail(); + MatchMakingKeyValuePair_t &keyvalue = vecServerFilters[idx]; + Q_strncpy( keyvalue.m_szKey, pchKey, sizeof( keyvalue.m_szKey ) ); + Q_strncpy( keyvalue.m_szValue, pchValue, sizeof( keyvalue.m_szValue ) ); + } + + static void AddMapsFilter( CUtlVector<MatchMakingKeyValuePair_t> &vecServerFilters, EGameCategory t ) + { + CUtlString sMapList; + for ( int i = 0 ; i < GetItemSchema()->GetMapCount() ; ++i ) + { + const SchemaMap_t& map = GetItemSchema()->GetMapForIndex( i ); + int mapType = map.eGameCategory; + if ( ( mapType == t ) || ( ( mapType == kGameCategory_Event247 ) && ( t == kGameCategory_EventMix ) ) ) + { + if ( !sMapList.IsEmpty() ) + { + sMapList.Append( "," ); + } + sMapList.Append( map.pszMapName ); + } + } + MatchMakingKeyValuePair_t kludge; + if ( sMapList.Length() < sizeof( kludge.m_szValue ) ) + { + AddFilter( vecServerFilters, "map", sMapList ); + } + else + { + Warning( "List of map names too long for this game mode, cannot filter on server side!\n" ); + Assert( false ); + } + } + + bool PassesFilter( const gameserveritem_t &server ) + { + if ( server.m_nAppID != m_nAppID ) + { + // This should be filtered by the steam matchmaking servers API + AssertMsg( server.m_nAppID == 0, "Got back server with wrong APP ID?" ); + return false; + } + + if ( server.m_bPassword ) + return false; + + if ( GetUniverse() == k_EUniversePublic ) + { + if ( server.m_bSecure == false ) + return false; + } + + if ( server.m_nMaxPlayers > kTFQuickPlayMaxPlayers ) + return false; + if ( server.m_nMaxPlayers < kTFQuickPlayMinMaxNumberOfPlayers ) + return false; + switch ( m_options.m_eMaxPlayers ) + { + case QuickplaySearchOptions::eMaxPlayers24: + if ( server.m_nMaxPlayers != 24 ) + return false; + break; + + case QuickplaySearchOptions::eMaxPlayers30Plus: + if ( server.m_nMaxPlayers < 30 ) + return false; + break; + + default: + Assert( false ); + case QuickplaySearchOptions::eMaxPlayersDontCare: + break; + } + + if ( server.m_steamID.BGameServerAccount() == false ) + return false; + + return true; + } + + void CalculateRecentMatchPenalty( sortable_gameserveritem_t &item ) + { + // Hysteresis + int iRecentIndex = FindRecentlyMatchedServer( item.server.m_NetAdr.GetIP(), item.server.m_NetAdr.GetConnectionPort() ); + item.m_fRecentMatchPenalty = 0.0f; + if ( iRecentIndex >= 0 ) + { + float fAge = (float)(CRTime::RTime32TimeCur() - s_vecRecentlyMatchedServers[iRecentIndex].m_timeMatched); + if ( fAge < 0.0f ) + { + // Time running in reverse?! + Assert( fAge >= 0.0f ); + } + else + { + float fCooldownTime = tf_matchmaking_retry_cooldown_seconds.GetFloat(); + Assert( fCooldownTime > 0.0f ); + if ( fCooldownTime > 0.0f ) // 0 can be used to turn this off, but then all the entries should immediately expire and we shouldn't be here... + { + float fAgePct = MIN( fAge / fCooldownTime, 1.0f ); + item.m_fRecentMatchPenalty = (1.0f - fAgePct) * tf_matchmaking_retry_max_penalty.GetFloat(); + } + } + } + } + + virtual void OnThink() + { + BaseClass::OnThink(); + + double currentTime = Plat_FloatTime(); + + // Do work, depending on the current step + switch ( m_eCurrentStep ) + { + case k_EStep_GMSQuery: + CheckSendScoresToGC(); + break; + + case k_EStep_GCScore: + // Check for timeout + Assert( m_timeGCScoreTimeout > 0.0 ); + if ( currentTime > m_timeGCScoreTimeout ) + { + + // Well, we never heard back from the GC. But we needn't let that stop us. + // We have quite a bit of data, let's just go with what we have, rather + // than totally failing + C_CTFGameStats::ImmediateWriteInterfaceEvent( "response(quickplay)", "gc_score_timeout" ); + Warning( "Timed out waiting to get scores back from GC, proceding without reputation data.\n" ); + + CMsgTFQuickplay_ScoreServersResponse emptyMsg; + OnReceivedGCScores( emptyMsg ); + } + break; + + case k_EStep_SelectServerUI: + // !FIXME! Ping servers every now and then and remove the full ones? + break; + + case k_EStep_PingCheckNotFull: + + // Do our own timeout processing + Assert( m_hServerQueryRequest != HSERVERQUERY_INVALID ); + if ( currentTime > m_timePingServerTimeout || m_hServerQueryRequest == HSERVERQUERY_INVALID ) + { + ServerFailedToRespond(); + } + break; + + case k_EStep_Terminated: + // Just wait to die + break; + + default: + Assert( false ); + SendStats( TF_Gamestats_QuickPlay_t::k_Result_InternalError ); + GenericFailure(); + break; + } + } + + // Called when we're done, just before we go on to wherever we're going next + void CloseMe() + { + m_eCurrentStep = k_EStep_Terminated; + DestroyServerListRequest(); + DestroyServerQueryRequest(); + CloseWaitingDialog(); + } + + virtual void GenericFailure() + { + CloseMe(); + ShowMessageBox( "#TF_MM_GenericFailure_Title", "#TF_MM_GenericFailure", "#GameUI_OK", &OpenQuickplayDialogCallback ); + } + + virtual void NoServersFoundFailure() + { + CloseMe(); + ShowMessageBox( "#TF_MM_ResultsDialog_Title", "#TF_MM_ResultsDialog_ServerNotFound", "#GameUI_OK", &OpenQuickplayDialogCallback ); + } + + void CheckSendScoresToGC() + { + + // How long have we been searching? + Assert( m_timeGMSSearchStarted > 0.0 ); + float fSearchDuration = Plat_FloatTime() - m_timeGMSSearchStarted; + + // Get a list of all the servers worth joining, and sort them + float fScoreThreshold = 1.0f; // !FIXME! Put into convar + CUtlSortVector< sortable_gameserveritem_t *, CGameServerItemSort > vecGameServers; + FOR_EACH_MAP_FAST( m_mapServers, idx ) + { + sortable_gameserveritem_t *pItem = &m_mapServers[idx]; + if ( pItem->TotalScore() > fScoreThreshold ) + { + vecGameServers.InsertNoSort( pItem ); + } + } + vecGameServers.RedoSort(); + + // See how long we've been searching, relative to the max time + Assert( tf_matchmaking_max_search_time.GetFloat() > 0.0 ); // don't be dumb + float fSearchTimePct = fSearchDuration / tf_matchmaking_max_search_time.GetFloat(); + + // Give up based on sheer timeout? + if ( fSearchTimePct > 1.0 ) + { + // Time to give up + TF_MATCHMAKING_SPEW(1, "Quickplay GMS search timed out. We'll go with what we've found so far.\n" ); + DestroyServerListRequest(); + } + + // Check if we should keep searching + if ( m_hServerListRequest ) + { + + // Determine "good enough" tolerances + float fScoreLo = Lerp( fSearchTimePct, tf_matchmaking_goodenough_score_start.GetFloat(), tf_matchmaking_goodenough_score_end.GetFloat() ); + float fCountLo = Lerp( fSearchTimePct, tf_matchmaking_goodenough_count_start.GetFloat(), tf_matchmaking_goodenough_count_end.GetFloat() ); + //float fScoreHi = Lerp( fSearchTimePct, tf_matchmaking_goodenough_hi_score_start.GetFloat(), tf_matchmaking_goodenough_hi_score_end.GetFloat() ); + //float fCountHi = Lerp( fSearchTimePct, tf_matchmaking_goodenough_hi_count_start.GetFloat(), tf_matchmaking_goodenough_hi_count_end.GetFloat() ); + + // !TEST! Hack this to force us to just show some servers + //fScoreLo = 3.0f; + //fCountLo = 10.0f; + + // Now scan list to see if we have enough results at this point + int iGoodEnoughCount = 0; + for ( iGoodEnoughCount = 0 ; iGoodEnoughCount < vecGameServers.Count() ; ++iGoodEnoughCount) + { + sortable_gameserveritem_t *pItem = vecGameServers[ iGoodEnoughCount ]; + if ( pItem->TotalScore() < fScoreLo ) + { + // This guy and all subsequent ones don't meet the criteria. + break; + } + Assert( pItem->m_eStatus == TF_Gamestats_QuickPlay_t::k_Server_Eligible ); + } + + // See how many more are eligible, just not meeting our criteria to terminate the search + int iEligibleCount = iGoodEnoughCount; + while ( iEligibleCount < vecGameServers.Count() && vecGameServers[iEligibleCount]->m_eStatus > TF_Gamestats_QuickPlay_t::k_Server_Ineligible ) + { + Assert( vecGameServers[iEligibleCount]->m_eStatus == TF_Gamestats_QuickPlay_t::k_Server_Eligible ); + ++iEligibleCount; + } + + // Update the dialog + if ( m_pBusyContainer ) + { + locchar_t wszValue[64]; + locchar_t wszText[256]; + loc_sprintf_safe( wszValue, LOCCHAR( "%u" ), iEligibleCount ); + g_pVGuiLocalize->ConstructString_safe( wszText, g_pVGuiLocalize->Find( "#TF_Quickplay_NumGames" ), 1, wszValue ); + m_pBusyContainer->SetDialogVariable( "numservers", wszText ); + } + + // Found enough good servers to bail now? + if ( iGoodEnoughCount >= (int)(fCountLo + 0.5f) ) + { + TF_MATCHMAKING_SPEW(1, "Quickplay initial search: %d servers with scores %.2f or better. Good enough...proceeding to backend scoring.\n", iGoodEnoughCount, fScoreLo ); + } + else + { + // We haven't found enough good servers. Update progress indicator + // to show that we're doing something + if ( m_pProgressBar ) + { + // Determine percentage completion based on "good enough" criteria. + float fGoodEnoughPct = (float)iGoodEnoughCount / (float)fCountLo; + + // Use max of this indicator, or basic timeout. That way, + // there is always some motion on the progress bar + float fProgressPct = MAX( fGoodEnoughPct, fSearchTimePct ); + + // Set it + m_pProgressBar->SetProgress( fProgressPct ); + } + + // Keep searching + return; + } + + // OK, we think we've got enough decent servers to see what the GC thinks. + // No need to keep talking to the GMS + DestroyServerListRequest(); + } + + // Set progress bar to 100% + if ( m_pProgressBar ) + { + m_pProgressBar->SetProgress( 1.0f ); + } + + // Decide how many to score. + const int iNumServersToScore = MIN( vecGameServers.Count(), kTFMaxQuickPlayServersToScore ); + + // + // Dump some debug stuff to the console + // + TF_MATCHMAKING_SPEW( 1, "Quickplay client scoring\n" ); + TF_MATCHMAKING_SPEW( 1, "Selected game mode: %d\n", m_options.m_eSelectedGameType ); + TF_MATCHMAKING_SPEW( 1, "Hours played: %.1f\n", m_fHoursPlayed ); + TF_MATCHMAKING_SPEW( 1, "Valve server bonus: %.2f, noob map bonus: %.2f\n", m_fValveBonus, m_fNoobMapBonus ); + TF_MATCHMAKING_SPEW( 1, "Search duration: %.1f\n", fSearchDuration ); + TF_MATCHMAKING_SPEW( 1, "Found %d total servers, %d met minimum score threshold, %d will be sent to GC for scoring\n", m_mapServers.Count(), vecGameServers.Count(), iNumServersToScore ); + CUtlSortVector< sortable_gameserveritem_t *, CGameServerItemSort > vecGameServersToPrint; + if ( tf_matchmaking_debug.GetInt() >= 4 ) + { + FOR_EACH_MAP_FAST( m_mapServers, idx ) + { + vecGameServersToPrint.InsertNoSort( &m_mapServers[idx] ); + } + } + else if ( tf_matchmaking_debug.GetInt() >= 3 ) + { + for ( int i = 0 ; i < vecGameServers.Count() ; ++i) + { + vecGameServersToPrint.InsertNoSort( vecGameServers[i] ); + } + } + else if ( tf_matchmaking_debug.GetInt() >= 2 ) + { + for ( int i = 0 ; i < iNumServersToScore ; ++i) + { + vecGameServersToPrint.InsertNoSort( vecGameServers[i] ); + } + } + if ( vecGameServersToPrint.Count() > 0 ) + { + vecGameServersToPrint.RedoSort(); + Msg( "Name, address, numplayers, maxplayers, ping, new user friendly, registered, valve, recent match penalty, user score, server score, total score, steamID, map, tags\n" ); + + // Print in reverse order --- the bottom is the most interesting part and the log usually scrolls. + // Anybody looking at a file can pull it into excel and sort however they want + for ( int i = vecGameServersToPrint.Count()-1 ; i >=0 ; --i ) + { + sortable_gameserveritem_t &item = *vecGameServersToPrint[i]; + Msg( "\"%s\",\"%s\",%d,%d,%d,%d,%d,%d,%.2f,%7.4f,%7.4f,%7.4f,\"%s\",\"%s\",\"%s\"\n", + item.server.GetName(), + item.server.m_NetAdr.GetConnectionAddressString(), + item.server.m_nPlayers - item.server.m_nBotPlayers, item.server.m_nMaxPlayers, item.server.m_nPing, + item.m_bNewUserFriendly ? 1 : 0, + item.m_bRegistered ? 1 : 0, + item.m_bValve ? 1 : 0, + item.m_fRecentMatchPenalty, + item.userScore, item.serverScore, item.serverScore+item.userScore, + item.server.m_steamID.Render(), + item.server.m_szMap, + item.server.m_szGameTags + ); + } + } + + // *NOW* Check for failure. (We waited until now, so we can properly log it.) + if ( iNumServersToScore <= 0 ) + { + C_CTFGameStats::ImmediateWriteInterfaceEvent( "response(quickplay)", "servers_not_found" ); + SendStats( m_mapServers.Count() > 0 ? + TF_Gamestats_QuickPlay_t::k_Result_NoServersMetCrtieria : + TF_Gamestats_QuickPlay_t::k_Result_NoServersFound ); + NoServersFoundFailure(); + return; + } + + // send message to GC + GCSDK::CProtoBufMsg< CMsgTFQuickplay_ScoreServers > msg( k_EMsgGC_QP_ScoreServers ); + for ( int i = 0; i < iNumServersToScore; ++i ) + { + sortable_gameserveritem_t *pItem = vecGameServers[i]; + gameserveritem_t &server = pItem->server; + Assert( pItem->m_eStatus == TF_Gamestats_QuickPlay_t::k_Server_Eligible ); + pItem->m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_RequestedScore; + CMsgTFQuickplay_ScoreServers_ServerInfo *pServerInfo = msg.Body().add_servers(); + pServerInfo->set_server_address( server.m_NetAdr.GetIP() ); + pServerInfo->set_server_port( server.m_NetAdr.GetConnectionPort() ); + pServerInfo->set_num_users( server.m_nPlayers - server.m_nBotPlayers ); + pServerInfo->set_max_users( server.m_nMaxPlayers ); + pServerInfo->set_user_score( pItem->userScore ); + pServerInfo->set_steam_id( server.m_steamID.ConvertToUint64() ); + } + + m_eCurrentStep = k_EStep_GCScore; + + //// !TEST! Skip scoring + //CMsgTFQuickplay_ScoreServersResponse emptyMsg; + //OnReceivedGCScores( emptyMsg ); + //return; + + GCClientSystem()->BSendMessage( msg ); + + // Give the GC some time to respond. It usually wil respond very quickly + m_timeGCScoreTimeout = Plat_FloatTime() + 10.0f; + } + + void UserConnectToServer() + { + Assert( m_eCurrentStep == k_EStep_SelectServerUI ); + if ( m_adrCurrentPing.IsValid() ) + return; + CQuickListPanel *pPanel = dynamic_cast<CQuickListPanel*>( m_pServerListPanel->GetSelectedPanel() ); + if ( pPanel == NULL ) + return; + int idx = pPanel->GetListID(); + m_adrCurrentPing = m_mapServers.Key( idx ); + PingServer(); + } + + void QuickpickPingFailed( const char *pszLocTok ) + { + ShowMessageBox( "#TF_Quickplay_Error", pszLocTok, "#GameUI_OK" ); + + // Remove this guy + for ( int i = 0 ; i < m_vecServerJoinQueue.Count() ; ++i ) + { + if ( m_vecServerJoinQueue[i].m_adr == m_adrCurrentPing ) + { + m_vecServerJoinQueue.Remove(i); + if ( m_pServerListPanel ) + m_pServerListPanel->RemoveItem(i); + break; + } + } + + m_adrCurrentPing.Clear(); + + // !KLUDGE! If the list goes empty, then just act like they hit ESC. + if ( m_pServerListPanel && m_pServerListPanel->GetItemCount() == 0 ) + OnCommand( "user_close" ); + } + + void PingNextBestServer() + { + Assert( m_eCurrentStep == k_EStep_PingCheckNotFull ); + + // If we already have any ping activity going, cancel it. (We shouldn't) + DestroyServerQueryRequest(); + + // Any more options to try? + if ( m_vecServerJoinQueue.Count() < 1 ) + { + TF_MATCHMAKING_SPEW( 1, "No more scored servers left to ping. We failed!\n" ); + SendStats( TF_Gamestats_QuickPlay_t::k_Result_FinalPingFailed ); + NoServersFoundFailure(); + return; + } + + // Remove the next address from the queue + m_adrCurrentPing = m_vecServerJoinQueue[0].m_adr; + m_vecServerJoinQueue.Remove( 0 ); + + // Do it + PingServer(); + } + + void PingServer() + { + // If we already have any ping activity going, cancel it. (We shouldn't) + DestroyServerQueryRequest(); + + // Find it in the server list; mark it as being pinged + int iServerIndex = m_mapServers.Find( m_adrCurrentPing ); + if ( m_mapServers.IsValidIndex(iServerIndex) ) + { + Assert( m_mapServers[iServerIndex].m_eStatus >= TF_Gamestats_QuickPlay_t::k_Server_Eligible ); + m_mapServers[iServerIndex].m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_Pinged; + } + else + { + // Wat. + Assert( m_mapServers.IsValidIndex(iServerIndex) ); + } + + // Send the ping + TF_MATCHMAKING_SPEW( 2, "Pinging %s\n", m_adrCurrentPing.ToString() ); + m_hServerQueryRequest = steamapicontext->SteamMatchmakingServers()->PingServer( m_adrCurrentPing.GetIPHostByteOrder(), m_adrCurrentPing.GetPort(), this ); + Assert( m_hServerQueryRequest != HSERVERQUERY_INVALID ); + + // Set timeout. Since we've already pinged them once + // fairly recently, and we are considering joining this + // server, we don't want to wait around forever. They + // should reply to the ping pretty quickly or let's + // not join them. + m_timePingServerTimeout = Plat_FloatTime() + 1.0; + } + + virtual void OnKeyCodeTyped( vgui::KeyCode code ) + { + if( code == KEY_ESCAPE ) + { + OnCommand( "user_close" ); + } + else + { + BaseClass::OnKeyCodeTyped( code ); + } + } + +}; + +class CStandaloneQuickplayMenu : public CQuickplayWaitDialog +{ + DECLARE_CLASS_SIMPLE( CStandaloneQuickplayMenu, CQuickplayWaitDialog ); +public: + CStandaloneQuickplayMenu( bool bFeelingLucky, const QuickplaySearchOptions &opt ) + : BaseClass( bFeelingLucky, opt ) + {} + + virtual void GenericFailure() OVERRIDE + { + CloseMe(); + ShowMessageBox( "#TF_MM_GenericFailure_Title", "#TF_MM_GenericFailure", "#GameUI_OK", NULL ); + } + + virtual void NoServersFoundFailure() OVERRIDE + { + CloseMe(); + ShowMessageBox( "#TF_MM_ResultsDialog_Title", "#TF_MM_ResultsDialog_ServerNotFound", "#GameUI_OK", NULL ); + } +}; + +//----------------------------------------------------------------------------- +// Purpose: Quickplay Dialog +//----------------------------------------------------------------------------- +ConVar tf_quickplay_lastviewedmode( "tf_quickplay_lastviewedmode", "0", FCVAR_CLIENTDLL | FCVAR_ARCHIVE ); + +CQuickplayPanelBase::CQuickplayPanelBase( vgui::Panel *parent, const char *name ) +: vgui::EditablePanel( parent, name ) +, m_iCurrentItem( 0 ) +, m_pContainer( NULL ) +, m_bSetInitialSelection( false ) +, m_pPrevPageButton( NULL ) +, m_pNextPageButton( NULL ) +, m_bShowRandomOption( true ) +, m_pSimplifiedOptionsContainer( NULL ) +, m_pAdvOptionsContainer( NULL ) +{ + vgui::HScheme scheme = vgui::scheme()->LoadSchemeFromFileEx( enginevgui->GetPanel( PANEL_CLIENTDLL ), "resource/ClientScheme.res", "ClientScheme" ); + SetScheme(scheme); + SetProportional( true ); +} +CQuickplayPanelBase::~CQuickplayPanelBase() +{ + vgui::ivgui()->RemoveTickSignal( GetVPanel() ); +} + +void CQuickplayPanelBase::ShowItemByGameType( EGameCategory type ) +{ + for ( int i = 0 ; i < m_vecItems.Count() ; ++i ) + { + if ( m_vecItems[i].gameType == type ) + { + ShowItemByIndex( i ); + return; + } + } + + AssertMsg1( false, "Bogus quickplay type %d", type ); +} + +void CQuickplayPanelBase::ShowItemByIndex( int iItem ) +{ + Assert( iItem >= 0 ); + Assert( iItem < m_vecItems.Size() ); + m_iCurrentItem = iItem; + + QuickplayItem &item = m_vecItems[ m_iCurrentItem ]; + + if ( m_pGameModeInfoContainer ) + { + m_pGameModeInfoContainer->SetDialogVariable( "gametype", g_pVGuiLocalize->Find( item.pTitle ) ); + m_pGameModeInfoContainer->SetDialogVariable( "description", g_pVGuiLocalize->Find( item.pDescription ) ); + m_pGameModeInfoContainer->SetDialogVariable( "complexity", g_pVGuiLocalize->Find( item.pComplexity ) ); + + if ( m_pMoreInfoContainer ) + m_pMoreInfoContainer->SetDialogVariable( "more_info", g_pVGuiLocalize->Find( item.pMoreInfo ) ); + + vgui::ImagePanel *pImage = dynamic_cast< vgui::ImagePanel* >( m_pGameModeInfoContainer->FindChildByName( "ModeImage" ) ); + if ( pImage ) + { + pImage->SetImage( GetItemImage( item ) ); + } + } + + char szTmp[16]; + Q_snprintf(szTmp, 16, "%d/%d", m_iCurrentItem + 1, m_vecItems.Size() ); + if ( m_pSimplifiedOptionsContainer ) + m_pSimplifiedOptionsContainer->SetDialogVariable( "page", szTmp ); + + if ( m_pGameModeCombo ) + m_pGameModeCombo->SilentActivateItemByRow( iItem ); + + WriteOptionCombosAndSummary(); +} + +const char *CQuickplayPanelBase::GetItemImage( const QuickplayItem& item ) const +{ + return item.pImage; +} + +void CQuickplayPanelBase::SetupActionTarget( const char *pPanelName ) +{ + vgui::Panel *pPanel = m_pContainer->FindChildByName( pPanelName, true ); + if ( pPanel ) + { + pPanel->AddActionSignalTarget( this ); + } +} + +void CQuickplayPanelBase::AddItem( EGameCategory gameType, const char *pTitle, const char *pDescription, const char *pMoreInfo, const char *pComplexity, const char *pImage, const char *pBetaImage ) +{ + int idx = m_vecAllItems.AddToTail(); + QuickplayItem &item = m_vecAllItems[idx]; + item.gameType = gameType; + item.pTitle = pTitle; + item.pDescription = pDescription; + item.pMoreInfo = pMoreInfo; + item.pComplexity = pComplexity; + item.pImage = pImage; + item.pBetaImage = pBetaImage; + + if ( m_pGameModeCombo ) + m_pGameModeCombo->AddItem( pTitle, NULL ); +} + +void CQuickplayPanelBase::UpdateSelectableItems() +{ + m_vecItems = m_vecAllItems; +} + +void CQuickplayPanelBase::ApplySettings( KeyValues *pInResourceData ) +{ + BaseClass::ApplySettings( pInResourceData ); + + V_strcpy_safe( m_szEvent247Image, pInResourceData->GetString( "event247_image", "illustrations/gamemode_halloween" ) ); + V_strcpy_safe( m_szCommunityUpdateImage, pInResourceData->GetString( "community_update_image", "illustrations/gamemode_cp" ) ); +} + +void CQuickplayPanelBase::ApplySchemeSettings( vgui::IScheme *pScheme ) +{ + BaseClass::ApplySchemeSettings( pScheme ); + + m_pSimplifiedOptionsContainer = dynamic_cast< vgui::EditablePanel *>( FindChildByName( "SimplifiedOptionsContainer", true ) ); Assert( m_pSimplifiedOptionsContainer ); + if ( m_pSimplifiedOptionsContainer ) + { + m_pGameModeInfoContainer = dynamic_cast< vgui::EditablePanel *>( m_pSimplifiedOptionsContainer->FindChildByName( "ModeInfoContainer", true ) ); Assert( m_pGameModeInfoContainer ); + } + if ( m_pGameModeInfoContainer ) + { + m_pMoreInfoContainer = dynamic_cast< vgui::EditablePanel *>( m_pGameModeInfoContainer->FindChildByName( "MoreInfoContainer", true ) ); Assert( m_pMoreInfoContainer ); + } + m_pPrevPageButton = dynamic_cast< vgui::Button *>( FindChildByName( "PrevPageButton", true ) ); Assert( m_pPrevPageButton ); + m_pNextPageButton = dynamic_cast< vgui::Button *>( FindChildByName( "NextPageButton", true ) ); Assert( m_pNextPageButton ); + m_pMoreOptionsButton = dynamic_cast< vgui::Button *>( FindChildByName( "OptionsButton", true ) ); + m_pAdvOptionsContainer = dynamic_cast< vgui::EditablePanel *>( FindChildByName( "AdvOptionsContainer", true ) ); + m_pGameModeCombo = NULL; + m_pOptionsSummaryLabel = NULL; + if ( m_pAdvOptionsContainer ) + { + Panel *pGameModeOptionContainer = m_pAdvOptionsContainer->FindChildByName( "GameModeOptionContainer", true ); Assert( pGameModeOptionContainer ); + + if ( pGameModeOptionContainer ) + { + m_pGameModeCombo = dynamic_cast< vgui::ComboBox *>( pGameModeOptionContainer->FindChildByName( "OptionCombo", true ) ); + Assert( m_pGameModeCombo ); + } + if ( m_pGameModeCombo ) + m_pGameModeCombo->AddActionSignalTarget( this ); + if ( m_pContainer ) + { + m_pOptionsSummaryLabel = dynamic_cast< vgui::Label *>( m_pContainer->FindChildByName( "OptionsSummaryLabel", true ) ); + Assert( m_pOptionsSummaryLabel ); + } + } + + //m_pFavoritesCheckButton = dynamic_cast< vgui::CheckButton *>( m_pContainer->FindChildByName( "FavoritesCheckButton" ) ); + //m_pFavoritesCheckButton->AddActionSignalTarget( this ); + // + //m_pRefreshButton = dynamic_cast< vgui::Button *>( m_pContainer->FindChildByName( "RefreshButton" ) ); + //m_pRefreshButton->AddActionSignalTarget( this ); + + m_vecItems.RemoveAll(); + m_vecAllItems.RemoveAll(); + + // listed in the order we want to show them + extern bool TF_IsHolidayActive( int eHoliday ); + bool bHalloween = TF_IsHolidayActive( kHoliday_Halloween ); + if ( bHalloween ) + { + AddItem( kGameCategory_Event247, "#Gametype_Halloween247", "#TF_GameModeDesc_Halloween247", "#TF_GameModeDetail_Halloween247", "#TF_Quickplay_Complexity1", m_szEvent247Image, NULL ); + AddItem( kGameCategory_EventMix, "#Gametype_HalloweenMix", "#TF_GameModeDesc_HalloweenMix", "#TF_GameModeDetail_HalloweenMix", "#TF_Quickplay_Complexity1", "illustrations/gamemode_halloween", NULL ); + } +// AddItem( kGameType_Community_Update,"#GameType_Community_Update", "#TF_GameModeDesc_Community_Update", "#TF_GameModeDetail_Community_Update", "#TF_Quickplay_Complexity1", m_szCommunityUpdateImage, NULL ); +// AddItem( kGameType_Featured, "#GameType_Featured", "#TF_GameModeDesc_Featured", "#TF_GameModeDetail_Featured", "#TF_Quickplay_Complexity1", "illustrations/gamemode_operation_tough_break", NULL ); + AddItem( kGameCategory_Escort, "#Gametype_Escort", "#TF_GameModeDesc_Escort", "#TF_GameModeDetail_Escort", "#TF_Quickplay_Complexity1", "illustrations/gamemode_payload", "illustrations/gamemode_payload_beta" ); + AddItem( kGameCategory_Koth, "#Gametype_Koth", "#TF_GameModeDesc_Koth", "#TF_GameModeDetail_Koth", "#TF_Quickplay_Complexity1", "illustrations/gamemode_koth", NULL ); + AddItem( kGameCategory_AttackDefense, "#Gametype_AttackDefense", "#TF_GameModeDesc_AttackDefense", "#TF_GameModeDetail_AttackDefense", "#TF_Quickplay_Complexity1", "illustrations/gamemode_attackdefend", NULL ); + AddItem( kGameCategory_EscortRace, "#Gametype_EscortRace", "#TF_GameModeDesc_EscortRace", "#TF_GameModeDetail_EscortRace", "#TF_Quickplay_Complexity2", "illustrations/gamemode_payloadrace", NULL ); + AddItem( kGameCategory_CP, "#Gametype_CP", "#TF_GameModeDesc_CP", "#TF_GameModeDetail_CP", "#TF_Quickplay_Complexity2", "illustrations/gamemode_cp", NULL ); + AddItem( kGameCategory_CTF, "#Gametype_CTF", "#TF_GameModeDesc_CTF", "#TF_GameModeDetail_CTF", "#TF_Quickplay_Complexity2", "illustrations/gamemode_ctf", NULL ); + AddItem( kGameCategory_Misc, "#Gametype_Misc", "#TF_GameModeDesc_Misc", "#TF_GameModeDetail_Misc", "#TF_Quickplay_Complexity2", "illustrations/gamemode_sd", NULL ); + AddItem( kGameCategory_Powerup, "#GameType_Powerup", "#TF_GameModeDesc_Powerup", "#TF_GameModeDetail_Powerup", "#TF_Quickplay_Complexity3", "illustrations/gamemode_powerup", "illustrations/gamemode_powerup_beta" ); // Fix beta image once Heather has the image + AddItem( kGameCategory_Passtime, "#GameType_Passtime", "#TF_GameModeDesc_Passtime", "#TF_GameModeDetail_Passtime", "#TF_Quickplay_Complexity2", "illustrations/gamemode_passtime", "illustrations/gamemode_passtime_beta" ); + AddItem( kGameCategory_RobotDestruction,"#Gametype_RobotDestruction", "#TF_GameModeDesc_RobotDestruction", "#TF_GameModeDetail_RobotDestruction", "#TF_Quickplay_Complexity2", "illustrations/gamemode_sd", "illustrations/gamemode_robotdestruction_beta" ); + if ( m_bShowRandomOption ) + AddItem( kGameCategory_Quickplay, "#Gametype_Quickplay", "#TF_GameModeDesc_Quickplay", "#TF_GameModeDetail_Quickplay", "#TF_Quickplay_Complexity2", "illustrations/quickplay", "illustrations/quickplay_beta" ); + + // AddItem( kQuickplayGameType_Arena, "#Gametype_Arena", "#TF_GameModeDesc_Arena", "#TF_GameModeDetail_Arena", "#TF_Quickplay_Complexity3", "maps/menu_photos_cp_granary" ); + // AddItem( kQuickplayGameType_Specialty, "#Gametype_Specialty", "#TF_GameModeDesc_Specialty", "#TF_GameModeDetail_Specialty", "#TF_Quickplay_Complexity3", "maps/menu_photos_cp_granary" ); + + UpdateSelectableItems(); + SetupMoreOptions(); + + // set current, if we didn't already + if ( !m_bSetInitialSelection ) + { + m_iCurrentItem = tf_quickplay_lastviewedmode.GetInt(); + static bool bForcedOnce = false; + //tagES need to remove this when Operation Gun Mettle is finished + if ( /* bHalloween && */ !bForcedOnce ) + { + m_iCurrentItem = 0; + bForcedOnce = true; + } + m_bSetInitialSelection = true; + } + m_iCurrentItem = Min( m_iCurrentItem, m_vecItems.Count()-1 ); + m_iCurrentItem = Max( m_iCurrentItem, 0 ); + ShowItemByIndex( m_iCurrentItem ); + + SetupActionTarget( "MoreInfoButton" ); + SetupActionTarget( "PrevPageButton" ); + SetupActionTarget( "NextPageButton" ); + SetupActionTarget( "CancelButton" ); + + ShowSimplifiedOrAdvancedOptions(); +} + +void CQuickplayPanelBase::SetupMoreOptions() +{ + SetupActionTarget( "OptionsButton" ); + +// Panel *pOptionTemplate = m_pMoreOptionsContainer->FindChildByName( "OptionContainerTemplate" ); +// Assert( pOptionTemplate ); +// KeyValues *pTemplateSettings = new KeyValues( "Template" ); +// pOptionTemplate->GetSettings( pTemplateSettings ); +// pOptionTemplate->SetVisible( false ); +// +// new QuickplayOptionPanel( m_pMoreOptionsContainer, 0, pTemplateSettings ); +// new QuickplayOptionPanel( m_pMoreOptionsContainer, 1, pTemplateSettings ); +// new QuickplayOptionPanel( m_pMoreOptionsContainer, 2, pTemplateSettings ); +// +// pTemplateSettings->deleteThis(); + + AdvOption *pOpt; + COMPILE_TIME_ASSERT( kEAdvOption_ServerHost == 0 ); + pOpt = &m_vecAdvOptions[ m_vecAdvOptions.AddToTail() ]; + pOpt->m_pszContainerName = "ValveServerOption"; + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_ServerHost_Official" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_ServerHost_Official_Summary" ); + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_ServerHost_Community" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_ServerHost_Community_Summary" ); + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_ServerHost_DontCare" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_ServerHost_DontCare_Summary" ); + pOpt->m_pConvar = &tf_quickplay_pref_community_servers; + + COMPILE_TIME_ASSERT( kEAdvOption_MaxPlayers == 1 ); + pOpt = &m_vecAdvOptions[ m_vecAdvOptions.AddToTail() ]; + pOpt->m_pszContainerName = "IncreasedPlayerCountOption"; + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_MaxPlayers_Default" ); pOpt->m_vecOptionSummaryNames.AddToTail( "" ); + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_MaxPlayers_Increased" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_MaxPlayers_Increased_Summary" ); + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_MaxPlayers_DontCare" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_MaxPlayers_DontCare_Summary" ); + pOpt->m_pConvar = &tf_quickplay_pref_increased_maxplayers; + + COMPILE_TIME_ASSERT( kEAdvOption_Respawn == 2 ); + pOpt = &m_vecAdvOptions[ m_vecAdvOptions.AddToTail() ]; + pOpt->m_pszContainerName = "RespawnTimesOption"; + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_RespawnTimes_Default" ); pOpt->m_vecOptionSummaryNames.AddToTail( "" ); + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_RespawnTimes_Instant" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_RespawnTimes_Instant_Summary" ); + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_RespawnTimes_DontCare" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_RespawnTimes_DontCare_Summary" ); + pOpt->m_pConvar = &tf_quickplay_pref_respawn_times; + + COMPILE_TIME_ASSERT( kEAdvOption_RandomCrits == 3 ); + pOpt = &m_vecAdvOptions[ m_vecAdvOptions.AddToTail() ]; + pOpt->m_pszContainerName = "RandomCritsOption"; + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_RandomCrits_Default" ); pOpt->m_vecOptionSummaryNames.AddToTail( "" ); + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_RandomCrits_Disabled" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_RandomCrits_Disabled_Summary" ); + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_RandomCrits_DontCare" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_RandomCrits_DontCare_Summary" ); + pOpt->m_pConvar = &tf_quickplay_pref_disable_random_crits; + + COMPILE_TIME_ASSERT( kEAdvOption_DamageSpread == 4 ); + pOpt = &m_vecAdvOptions[ m_vecAdvOptions.AddToTail() ]; + pOpt->m_pszContainerName = "DamageSpreadOption"; + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_DamageSpread_Default" ); pOpt->m_vecOptionSummaryNames.AddToTail( "" ); + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_DamageSpread_Enabled" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_DamageSpread_Enabled_Summary" ); + pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_DamageSpread_DontCare" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_DamageSpread_DontCare_Summary" ); + pOpt->m_pConvar = &tf_quickplay_pref_enable_damage_spread; + + FOR_EACH_VEC( m_vecAdvOptions, idxAdvOpt ) + { + pOpt = &m_vecAdvOptions[ idxAdvOpt ]; + EditablePanel *pContainer = dynamic_cast<EditablePanel*>( FindChildByName( pOpt->m_pszContainerName, true ) ); + FOR_EACH_VEC( pOpt->m_vecOptionNames, idx ) + { + vgui::RadioButton *pRadioButton = NULL; + if ( pContainer ) + { + pRadioButton = dynamic_cast<vgui::RadioButton*>( pContainer->FindChildByName( VarArgs("RadioButton%d", idx ) ) ); + Assert( pRadioButton ); + } + if ( pRadioButton ) + { + pRadioButton->AddActionSignalTarget( this ); + pRadioButton->SetText( pOpt->m_vecOptionNames[idx] ); + } + pOpt->m_vecRadioButtons.AddToTail( pRadioButton ); + } + } + + // Populate choice from the convars + FOR_EACH_VEC( m_vecAdvOptions, idxAdvOpt ) + { + AdvOption *pOpt = &m_vecAdvOptions[ idxAdvOpt ]; + pOpt->m_nChoice = pOpt->m_pConvar->GetInt(); + } +} + +void CQuickplayPanelBase::ShowSimplifiedOrAdvancedOptions() +{ + if ( m_pSimplifiedOptionsContainer ) + m_pSimplifiedOptionsContainer->SetVisible( !tf_quickplay_pref_advanced_view.GetBool() ); + if ( m_pAdvOptionsContainer ) + m_pAdvOptionsContainer->SetVisible( tf_quickplay_pref_advanced_view.GetBool() ); +} + +const size_t kQuickplayOptionsSummaryLen = 1024; +static void AppendOptionInfo( wchar_t *wszText, const char *pszLocToken ) +{ + if ( pszLocToken == NULL || *pszLocToken == '\0' ) + return; + const wchar_t *pwszLocalized = NULL; + if ( *pszLocToken == '#' ) + pwszLocalized = g_pVGuiLocalize->Find( pszLocToken ); + wchar_t wszTemp[ 1024 ]; + if ( pwszLocalized == NULL ) + { + V_UTF8ToUnicode( pszLocToken, wszTemp, sizeof(wszTemp) ); + pwszLocalized = wszTemp; + } + if ( *wszText != '\0' ) + V_wcsncat( wszText, L"; ", kQuickplayOptionsSummaryLen ); + V_wcsncat( wszText, pwszLocalized, kQuickplayOptionsSummaryLen ); +} + +void CQuickplayPanelBase::WriteOptionCombosAndSummary() +{ + wchar_t wszSmmary[ kQuickplayOptionsSummaryLen ] = {}; + + GetOptionsAndSummaryText( wszSmmary ); + + if ( m_pOptionsSummaryLabel ) + m_pOptionsSummaryLabel->SetText( wszSmmary ); +} + +void CQuickplayPanelBase::GetOptionsAndSummaryText( wchar_t *pwszSummary ) +{ + if ( m_vecItems[m_iCurrentItem].gameType == kGameCategory_Quickplay ) + AppendOptionInfo( pwszSummary, "#Gametype_AnyGameMode" ); // "random" looks weird here, use custom text + else + AppendOptionInfo( pwszSummary, m_vecItems[m_iCurrentItem].pTitle ); + + // Check if we need to force "community only" or "don't care" server host due to their options + FOR_EACH_VEC( m_vecAdvOptions, idxVecOpt ) + { + if ( idxVecOpt == kEAdvOption_ServerHost ) + continue; + if ( m_vecAdvOptions[idxVecOpt].m_nChoice == 1 ) + { + m_vecAdvOptions[kEAdvOption_ServerHost].m_nChoice = QuickplaySearchOptions::eServersCommunity; + break; + } + } + + FOR_EACH_VEC( m_vecAdvOptions, idxAdvOpt ) + { + AdvOption *pOpt = &m_vecAdvOptions[ idxAdvOpt ]; + + FOR_EACH_VEC( pOpt->m_vecRadioButtons, idxRadButton ) + { + if ( pOpt->m_vecRadioButtons[idxRadButton] ) + { + //pOpt->m_vecRadioButtons[idxRadButton]->SetEnabled( idxRadButton == 0 || !bForceVanilla ); + pOpt->m_vecRadioButtons[idxRadButton]->SilentSetSelected( pOpt->m_nChoice == idxRadButton ); + } + } + + int nChoice = ( tf_quickplay_pref_beta_content.GetBool() ? 0 : pOpt->m_nChoice ); + AppendOptionInfo( pwszSummary, pOpt->m_vecOptionSummaryNames[nChoice] ); + } +} + +void CQuickplayPanelBase::OnTextChanged( vgui::Panel *panel ) +{ + if ( panel == m_pGameModeCombo ) + { + UserSelectItemByIndex( m_pGameModeCombo->GetActiveItem() ); + return; + } + +} + +void CQuickplayPanelBase::OnRadioButtonChecked( vgui::Panel *panel ) +{ + FOR_EACH_VEC( m_vecAdvOptions, idxAdvOpt ) + { + AdvOption *pOpt = &m_vecAdvOptions[ idxAdvOpt ]; + int idxBtn = pOpt->m_vecRadioButtons.Find( (vgui::RadioButton*)panel ); + if ( idxBtn >= 0 ) + { + pOpt->m_nChoice = idxBtn; + + // Check if they change the server hosting to valve servers, + // then slam their choices to vanilla + if ( idxAdvOpt == kEAdvOption_ServerHost && idxBtn == QuickplaySearchOptions::eServersOfficial ) + { + FOR_EACH_VEC( m_vecAdvOptions, idxSlam ) + { + if ( idxSlam != kEAdvOption_ServerHost ) + m_vecAdvOptions[idxSlam].m_nChoice = 0; + } + } + } + } + WriteOptionCombosAndSummary(); +} + +void CQuickplayPanelBase::SetPageScrollButtonsVisible( bool bFlag ) +{ + if ( m_pPrevPageButton ) + m_pPrevPageButton->SetVisible( bFlag ); + if ( m_pNextPageButton ) + m_pNextPageButton->SetVisible( bFlag ); +} + +void CQuickplayPanelBase::SaveSettings() +{ + tf_quickplay_lastviewedmode.SetValue( m_iCurrentItem ); + FOR_EACH_VEC( m_vecAdvOptions, idxAdvOpt ) + { + AdvOption *pOpt = &m_vecAdvOptions[ idxAdvOpt ]; + pOpt->m_pConvar->SetValue( pOpt->m_nChoice ); + } +} + +void CQuickplayPanelBase::OnCommand( const char *pCommand ) +{ + if ( FStrEq( pCommand, "prevpage" ) ) + { + UserSelectItemByIndex( ( m_iCurrentItem > 0 ) ? ( m_iCurrentItem - 1 ) : ( m_vecItems.Count() - 1 ) ); + if ( m_pPrevPageButton ) + m_pPrevPageButton->RequestFocus(); + } + else if ( FStrEq( pCommand, "nextpage" ) ) + { + UserSelectItemByIndex( ( m_iCurrentItem+1 < m_vecItems.Count() ) ? ( m_iCurrentItem+1 ) : 0 ); + if ( m_pNextPageButton ) + m_pNextPageButton->RequestFocus(); + } + else if ( FStrEq( pCommand, "more_info" ) ) + { + if ( m_pMoreInfoContainer && m_pGameModeInfoContainer ) + { + m_pMoreInfoContainer->SetVisible( !m_pMoreInfoContainer->IsVisible() ); + vgui::ImagePanel *pImage = dynamic_cast< vgui::ImagePanel* >( m_pGameModeInfoContainer->FindChildByName( "ModeImage" ) ); + if ( pImage ) + { + pImage->SetVisible( !m_pMoreInfoContainer->IsVisible() ); + } + } + } + else if ( FStrEq( pCommand, "ToggleShowOptions" ) ) + { + tf_quickplay_pref_advanced_view.SetValue( !tf_quickplay_pref_advanced_view.GetBool() ); + ShowSimplifiedOrAdvancedOptions(); + } + else + { + BaseClass::OnCommand( pCommand ); + } +} + +void CQuickplayPanelBase::UserSelectItemByIndex( int iNewItem ) +{ + ShowItemByIndex( iNewItem ); +} + + + +class CQuickplayDialog : public CQuickplayPanelBase +{ + DECLARE_CLASS_SIMPLE( CQuickplayDialog, CQuickplayPanelBase ); +public: + CQuickplayDialog( vgui::Panel *parent ) + : CQuickplayPanelBase( parent, "QuickPlayDialog" ) + { + m_pContainer = new vgui::EditablePanel( this, "Container" ); + m_pBetaCheckButton = new vgui::CheckButton( m_pContainer, "BetaCheckButton", "BetaToggle" ); + m_pBetaCheckButton->AddActionSignalTarget( this ); + m_pBetaCheckButton->SetSelected( tf_quickplay_pref_beta_content.GetInt() == 1 ); + m_pTauntsExplanationPopup = new CExplanationPopup( m_pContainer, "BetaExplanation" ); + + C_CTFGameStats::ImmediateWriteInterfaceEvent( "interface_open", "quickplay" ); + + LoadControlSettings( "Resource/ui/QuickplayDialog.res" ); + } + + virtual ~CQuickplayDialog() + { + + C_CTFGameStats::ImmediateWriteInterfaceEvent( "interface_close", "quickplay" ); + } + + virtual void ApplySchemeSettings( vgui::IScheme *pScheme ) OVERRIDE + { + BaseClass::ApplySchemeSettings( pScheme ); + + Panel *pPanel; + pPanel = FindChildByName( "PlayNowButton", true ); Assert( pPanel ); + if ( pPanel ) pPanel->AddActionSignalTarget( this ); + pPanel = FindChildByName( "ShowServersButton", true ); Assert( pPanel ); + if ( pPanel ) pPanel->AddActionSignalTarget( this ); + pPanel = FindChildByName( "ExplainBetaButton", true ); Assert( pPanel ); + if ( pPanel ) pPanel->AddActionSignalTarget( this ); + } + + virtual void PerformLayout() OVERRIDE + { + m_pBetaCheckButton->SizeToContents(); + + BaseClass::PerformLayout(); + + // Center it, keeping requested size + int x, y, ww, wt, wide, tall; + vgui::surface()->GetWorkspaceBounds( x, y, ww, wt ); + GetSize(wide, tall); + SetPos(x + ((ww - wide) / 2), y + ((wt - tall) / 2)); + + m_pMoreOptionsButton->SetVisible( !m_pBetaCheckButton->IsSelected() ); + tf_quickplay_pref_beta_content.SetValue( m_pBetaCheckButton->IsSelected() ? 1 : 0 ); + // @todo setup + } + + virtual void Show() + { + TFModalStack()->PushModal( this ); + + // Make sure we're signed on + if ( !CheckSteamSignOn() ) + { + Close(); + return; + } + + SetVisible( true ); + MakePopup(); + MoveToFront(); + SetKeyBoardInputEnabled( true ); + SetMouseInputEnabled( true ); + + vgui::Panel *pPlayNowButton = FindChildByName( "PlayNowButton" ); + if ( pPlayNowButton ) + { + pPlayNowButton->RequestFocus(); + } + } + + bool CheckSteamSignOn() + { + // Make sure we are connected to steam, or they are going to be disappointed + if ( steamapicontext == NULL + || steamapicontext->SteamUtils() == NULL + || steamapicontext->SteamMatchmakingServers() == NULL + || steamapicontext->SteamUser() == NULL + || !steamapicontext->SteamUser()->BLoggedOn() + ) { + Warning( "Steam not properly initialized or connected.\n" ); + ShowMessageBox( "#TF_MM_GenericFailure_Title", "#TF_MM_GenericFailure", "#GameUI_OK" ); + return false; + } + return true; + } + + virtual void OnCommand( const char *pCommand ) + { + C_CTFGameStats::ImmediateWriteInterfaceEvent( "on_command(quickplay)", pCommand ); + + if ( FStrEq( pCommand, "playnow" ) || FStrEq( pCommand, "show_servers" ) ) + { + SaveSettings(); + Close(); + + if ( !CheckSteamSignOn() ) + { + return; + } + + bool bBetaContent = tf_quickplay_pref_beta_content.GetBool(); + + QuickplaySearchOptions opt; + opt.m_eSelectedGameType = m_vecItems[m_iCurrentItem].gameType; + opt.m_eServers = (QuickplaySearchOptions::EServers)( bBetaContent ? +#ifdef STAGING_ONLY + 2 : +#else + 0 : +#endif + tf_quickplay_pref_community_servers.GetInt() ); + opt.m_eRandomCrits = (QuickplaySearchOptions::ERandomCrits)( bBetaContent ? 2 : tf_quickplay_pref_disable_random_crits.GetInt() ); + opt.m_eDamageSpread = (QuickplaySearchOptions::EDamageSpread)( bBetaContent ? 2 : tf_quickplay_pref_enable_damage_spread.GetInt() ); + opt.m_eRespawnTimes = (QuickplaySearchOptions::ERespawnTimes)( bBetaContent ? 2 : tf_quickplay_pref_respawn_times.GetInt() ); + opt.m_eMaxPlayers = (QuickplaySearchOptions::EMaxPlayers)( bBetaContent ? 2 : tf_quickplay_pref_increased_maxplayers.GetInt() ); + opt.m_eBetaContent = (QuickplaySearchOptions::EBetaContent)tf_quickplay_pref_beta_content.GetInt(); + ShowWaitingDialog( new CQuickplayWaitDialog( FStrEq( pCommand, "playnow" ), opt ), NULL, true, true, 0.0f ); + } + else if ( FStrEq( pCommand, "cancel" ) ) + { + Close(); + SaveSettings(); + } + else if ( FStrEq( pCommand, "beta_toggle" ) ) + { + UpdateSelectableItems(); + + if ( m_pMoreOptionsButton ) + { + // Disable the advanced filtering if beta box is checked + m_pMoreOptionsButton->SetVisible( !m_pBetaCheckButton->IsSelected() ); + tf_quickplay_pref_beta_content.SetValue( m_pBetaCheckButton->IsSelected() ? 1 : 0 ); + + WriteOptionCombosAndSummary(); + + tf_quickplay_pref_advanced_view.SetValue( 0 ); + ShowSimplifiedOrAdvancedOptions(); + } + } + else if ( FStrEq( pCommand, "explain_beta" ) ) + { + m_pTauntsExplanationPopup->Popup(); + } + else + { + BaseClass::OnCommand( pCommand ); + } + } + + virtual void OnKeyCodePressed( vgui::KeyCode code ) + { + vgui::KeyCode nButtonCode = GetBaseButtonCode( code ); + if( nButtonCode == KEY_XBUTTON_B || nButtonCode == STEAMCONTROLLER_B || nButtonCode == STEAMCONTROLLER_START ) + { + OnCommand( "cancel" ); + } + else if ( nButtonCode == KEY_XBUTTON_A || nButtonCode == STEAMCONTROLLER_A ) + { + OnCommand( "playnow" ); + } + else if ( nButtonCode == KEY_XBUTTON_X || nButtonCode == STEAMCONTROLLER_X ) + { + OnCommand( "more_info" ); + } + else if ( nButtonCode == KEY_XBUTTON_LEFT || + nButtonCode == KEY_XSTICK1_LEFT || + nButtonCode == KEY_XSTICK2_LEFT || + nButtonCode == STEAMCONTROLLER_DPAD_LEFT || + code == KEY_LEFT ) + { + OnCommand( "prevpage" ); + } + else if ( nButtonCode == KEY_XBUTTON_RIGHT || + nButtonCode == KEY_XSTICK1_RIGHT || + nButtonCode == KEY_XSTICK2_RIGHT || + nButtonCode == STEAMCONTROLLER_DPAD_RIGHT || + code == KEY_RIGHT ) + { + OnCommand( "nextpage" ); + } + else + { + BaseClass::OnKeyCodeTyped( code ); + } + } + + virtual void OnKeyCodeTyped( vgui::KeyCode code ) + { + if( code == KEY_ESCAPE ) + { + OnCommand( "cancel" ); + } + else if ( code == KEY_ENTER || code == KEY_SPACE ) + { + OnCommand( "playnow" ); + } + else + { + BaseClass::OnKeyCodeTyped( code ); + } + } + +protected: + + virtual const char *GetItemImage( const QuickplayItem& item ) const OVERRIDE + { + if ( m_pBetaCheckButton->IsSelected() && item.pBetaImage ) + { + return item.pBetaImage; + } + + return item.pImage; + } + + virtual void GetOptionsAndSummaryText( wchar_t *pwszSummary ) OVERRIDE + { + BaseClass::GetOptionsAndSummaryText( pwszSummary ); + + if ( m_pBetaCheckButton->IsSelected() ) + { + AppendOptionInfo( pwszSummary, "#TF_Quickplay_Beta" ); + } + } + + // called when the Cancel button is pressed + void Close() + { + SetVisible( false ); + TFModalStack()->PopModal( this ); + MarkForDeletion(); + } + + void UpdateSelectableItems() OVERRIDE + { + m_vecItems.Purge(); + + bool bBetaActive = m_pBetaCheckButton->IsSelected(); + + // Go through each of the modes + FOR_EACH_VEC( m_vecAllItems, i ) + { + int nNumWithBetaContent = 0; + int nNumForThisMode = 0; + + // Go through each of the modes + for ( int j = 0 ; j < GetItemSchema()->GetMapCount(); ++j ) + { + const SchemaMap_t& map = GetItemSchema()->GetMapForIndex( j ); + + // Tally up maps for this mode + if ( map.eGameCategory == m_vecAllItems[i].gameType ) + { + nNumForThisMode++; + + // Check if any of the tags has "beta" as a tag, and tally that if so + for( int k = 0; k < map.vecTags.Count(); ++k ) + { + if ( map.vecTags.HasElement( GetItemSchema()->GetHandleForTag( "beta" ) ) ) + { + nNumWithBetaContent++; + } + } + } + } + + // Only add the visible items if we're filtering for beta and this category has at least 1 beta map + // OR if we're not filtering for beta and we have at least 1 map that's not beta + const bool bBetaFilteringAndHasBetaContent = ( bBetaActive && nNumWithBetaContent > 0 ); + const bool bNotBetaFilteringHasNonBetaContent = ( !bBetaActive && nNumForThisMode > nNumWithBetaContent ); + const bool bIsRandom = m_vecAllItems[i].gameType == kGameCategory_Quickplay; + if ( ( bBetaFilteringAndHasBetaContent ) || ( bNotBetaFilteringHasNonBetaContent ) || bIsRandom ) + { + m_vecItems.AddToTail( m_vecAllItems[i] ); + } + } + + // Go back to the first page + ShowItemByIndex( 0 ); + } + +private: + + vgui::CheckButton *m_pBetaCheckButton; + CExplanationPopup *m_pTauntsExplanationPopup; +}; +static vgui::DHANDLE<CQuickplayDialog> g_pQuickplayDialog; + +//----------------------------------------------------------------------------- + +class CGCTFQuickplay_ScoreServers_Response : public GCSDK::CGCClientJob +{ +public: + CGCTFQuickplay_ScoreServers_Response( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} + + virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) + { + GCSDK::CProtoBufMsg<CMsgTFQuickplay_ScoreServersResponse> msg( pNetPacket ); + + if ( s_pQuickPlayWaitingDialog ) + { + // !TEST! Forced failure + //Warning( "CMsgTFQuickplay_ScoreServersResponse received, but discarded to simulate failure!!\n" ); + //return true; + + s_pQuickPlayWaitingDialog->OnReceivedGCScores( msg.Body() ); + } + else + { + Warning(" Received CGCTFQuickplay_ScoreServers_Response, but no quick play query in progress dialog to receive them?\n" ); + } + + return true; + } +}; +GC_REG_JOB( GCSDK::CGCClient, CGCTFQuickplay_ScoreServers_Response, "CGCTFQuickplay_ScoreServers_Response", k_EMsgGC_QP_ScoreServersResponse, GCSDK::k_EServerTypeGCClient ); + +//----------------------------------------------------------------------------- +// Purpose: Callback to open the game menus +//----------------------------------------------------------------------------- + +#ifdef ENABLE_GC_MATCHMAKING +ConVar tf_quickplay_beta_preference( "tf_quickplay_beta_preference", "-1", FCVAR_NONE, "Preference to participate in beta quickplay: -1 = no preference, 0 = opt out, 1 = opt in" ); +ConVar tf_quickplay_beta_ask_percentage( "tf_quickplay_beta_ask_percentage", "0", FCVAR_NONE, "Percentage of people who will be prompted to participate in beta quickplay." ); + + +void QuickplayBetaConfirmCallback( bool bConfirmed, void *pContext ) +{ + tf_quickplay_beta_preference.SetValue( bConfirmed ? 1 : 0 ); + if ( bConfirmed ) + { + engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby quickplay" ); + } + else + { + engine->ClientCmd_Unrestricted( "OpenQuickplayDialog nobeta" ); + } +} + +#endif // #ifdef ENABLE_GC_MATCHMAKING + +static void CL_OpenQuickplayDialog( const CCommand &args ) +{ + + // Check for opting into beta quickplay + #ifdef ENABLE_GC_MATCHMAKING + if ( args.ArgC() < 2 && tf_quickplay_beta_preference.GetInt() != 0 ) + { + if ( tf_quickplay_beta_preference.GetInt() > 0 ) + { + engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby quickplay" ); + return; + } + if ( steamapicontext && steamapicontext->SteamUser() && ( steamapicontext->SteamUser()->GetSteamID().GetAccountID() % 100U ) < (uint32)tf_quickplay_beta_ask_percentage.GetInt() ) + { + ShowConfirmDialog( "#TF_QuickplayBeta_OptIn_Title", "#TF_QuickplayBeta_OptIn_Message", "#TF_QuickplayBeta_OptIn_YesButton", "#TF_QuickplayBeta_OptIn_NoButton", QuickplayBetaConfirmCallback ); + return; + } + } + #endif + + if ( g_pQuickplayDialog.Get() == NULL ) + { + IViewPortPanel *pMMOverride = ( gViewPortInterface->FindPanelByName( PANEL_MAINMENUOVERRIDE ) ); + g_pQuickplayDialog = vgui::SETUP_PANEL( new CQuickplayDialog( (CHudMainMenuOverride*)pMMOverride ) ); + } + g_pQuickplayDialog->Show(); +} + +// the console commands +static ConCommand quickplaydialog( "OpenQuickplayDialog", &CL_OpenQuickplayDialog, "Displays the quickplay dialog." ); + + +static void CL_OpenQuickplayDialogForMap( const CCommand &args ) +{ + if ( args.ArgC() != 2 ) + return; + + QuickplaySearchOptions opt; + opt.m_eServers = QuickplaySearchOptions::eServersOfficial; // Quests only work on official. + // Default settings + opt.m_eRandomCrits = QuickplaySearchOptions::ERandomCrits::eRandomCritsYes; + opt.m_eDamageSpread = QuickplaySearchOptions::EDamageSpread::eDamageSpreadNo; + opt.m_eRespawnTimes = QuickplaySearchOptions::ERespawnTimes::eRespawnTimesDefault; + opt.m_eMaxPlayers = QuickplaySearchOptions::EMaxPlayers::eMaxPlayers24; + opt.m_eBetaContent = QuickplaySearchOptions::EBetaContent::eBetaNo; + opt.m_strMapName = args.Arg(1); // Use the map name passed in + opt.m_eSelectedGameType = kGameCategory_Quickplay; + + ShowWaitingDialog( new CStandaloneQuickplayMenu( false, opt ), NULL, true, true, 0.0f ); +} + +static ConCommand OpenQuickplayWaitDialogForMap( "OpenQuickplayWaitDialogForMap", &CL_OpenQuickplayDialogForMap, "Instantly starts a quickplay query for a map." ); |