|
|
//========= 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." );
|