Team Fortress 2 Source Code as on 22/4/2020
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3005 lines
100 KiB

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. //
  3. // Purpose:
  4. //
  5. // $NoKeywords: $
  6. //=============================================================================
  7. #include "cbase.h"
  8. // for messaging with the GC
  9. #include "tf_gcmessages.h"
  10. #include "tf_item_inventory.h"
  11. #include "econ_game_account_server.h"
  12. #include "gc_clientsystem.h"
  13. #include "tf_quickplay.h"
  14. // ui related
  15. #include "filesystem.h"
  16. #include "tf_controls.h"
  17. #include "clientmode_tf.h"
  18. #include "confirm_dialog.h"
  19. #include "econ_controls.h"
  20. #include "game/client/iviewport.h"
  21. #include "ienginevgui.h"
  22. #include "tf_hud_mainmenuoverride.h"
  23. #include "tf_hud_statpanel.h"
  24. #include "tf_mouseforwardingpanel.h"
  25. #include "vgui/ILocalize.h"
  26. #include "vgui/ISurface.h"
  27. #include "vgui_controls/PanelListPanel.h"
  28. #include "vgui_controls/ProgressBar.h"
  29. #include "vgui_controls/RadioButton.h"
  30. #include "ServerBrowser/blacklisted_server_manager.h"
  31. #include "rtime.h"
  32. #include "c_tf_gamestats.h"
  33. #include "tf_gamerules.h"
  34. #include "ServerBrowser/IServerBrowser.h"
  35. // other
  36. #include "tier1/netadr.h"
  37. // memdbgon must be the last include file in a .cpp file!!!
  38. #include <tier0/memdbgon.h>
  39. const char *COM_GetModDirectory();
  40. extern int g_iQuickplaySessionIndex;
  41. #include "tf_matchmaking_scoring.h"
  42. ConVar tf_matchmaking_debug( "tf_matchmaking_spew_level",
  43. #ifdef _DEBUG
  44. "3",
  45. #else
  46. "1",
  47. #endif
  48. FCVAR_NONE, "Set to 1 for basic console spew of quickplay-related decisions. 4 for maximum verbosity." );
  49. ConVar tf_quickplay_pref_community_servers( "tf_quickplay_pref_community_servers", "0", FCVAR_ARCHIVE, "0=Valve only, 1=Community only, 2=Either" );
  50. #define TF_MATCHMAKING_SPEW( lvl, pStrFmt, ...) \
  51. if ( tf_matchmaking_debug.GetInt() >= lvl ) \
  52. { \
  53. Msg( pStrFmt, ##__VA_ARGS__ ); \
  54. }
  55. // Hack to force it to search for other app ID's for testing
  56. static inline int GetTfMatchmakingAppID()
  57. {
  58. // !TEST!
  59. #ifdef STAGING_ONLY
  60. ConVarRef sb_fake_app_id( "sb_fake_app_id" );
  61. if ( sb_fake_app_id.GetInt() != 0 )
  62. return sb_fake_app_id.GetInt();
  63. #endif
  64. // return 440;
  65. return engine->GetAppID();
  66. }
  67. // A server that we recently were matched to and attempted to join
  68. struct RecentlyMatchedServer
  69. {
  70. uint32 m_ip;
  71. uint16 m_port;
  72. RTime32 m_timeMatched;
  73. };
  74. // Just store them in a simple list. This will be small, and it won't hurt
  75. // to scan the whole thing, so it won't do any good to do anything fancy
  76. static CUtlVector<RecentlyMatchedServer> s_vecRecentlyMatchedServers;
  77. // Search for a recently matched server. Also process cooldown expiry
  78. static int FindRecentlyMatchedServer( uint32 ip, uint16 port )
  79. {
  80. RTime32 timeOfOldestEntryToKeep = CRTime::RTime32TimeCur() - tf_matchmaking_retry_cooldown_seconds.GetInt();
  81. int i = 0;
  82. int result = -1;
  83. while ( i < s_vecRecentlyMatchedServers.Count() )
  84. {
  85. // Expire?
  86. if ( s_vecRecentlyMatchedServers[i].m_timeMatched < timeOfOldestEntryToKeep )
  87. {
  88. DevLog(" Expiring quickplay recently joined server entry %08X:%d\n", s_vecRecentlyMatchedServers[i].m_ip, (int)s_vecRecentlyMatchedServers[i].m_port );
  89. s_vecRecentlyMatchedServers.Remove( i );
  90. }
  91. else
  92. {
  93. // Address match?
  94. if ( s_vecRecentlyMatchedServers[i].m_ip == ip && s_vecRecentlyMatchedServers[i].m_port == port )
  95. {
  96. Assert( result < 0 ); // dups in the list?
  97. result = i;
  98. }
  99. // Record is still active. Keep it
  100. ++i;
  101. }
  102. }
  103. // Return index of the entry we found, if any
  104. return result;
  105. }
  106. ConVar tf_quickplay_pref_increased_maxplayers( "tf_quickplay_pref_increased_maxplayers", "0", FCVAR_ARCHIVE, "0=Default only, 1=Yes, 2=Don't care" );
  107. 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" );
  108. 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" );
  109. 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" );
  110. 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" );
  111. ConVar tf_quickplay_pref_beta_content( "tf_quickplay_pref_beta_content", "0", FCVAR_ARCHIVE, "0=No beta content, 1=Only beta content" );
  112. static bool BHasTag( const CUtlStringList &TagList, const char *tag )
  113. {
  114. for ( int i = 0; i < TagList.Count(); i++ )
  115. {
  116. if ( !Q_stricmp( TagList[i], tag) )
  117. {
  118. return true;
  119. }
  120. }
  121. return false;
  122. }
  123. static void GetQuickplayTags( const QuickplaySearchOptions &opt, CUtlStringList &requiredTags, CUtlStringList &illegalTags )
  124. {
  125. // Always required
  126. requiredTags.CopyAndAddToTail( "_registered" );
  127. // Always illegal
  128. illegalTags.CopyAndAddToTail( "friendlyfire" );
  129. illegalTags.CopyAndAddToTail( "highlander" );
  130. illegalTags.CopyAndAddToTail( "noquickplay" );
  131. illegalTags.CopyAndAddToTail( "trade" );
  132. switch ( opt.m_eRespawnTimes )
  133. {
  134. case QuickplaySearchOptions::eRespawnTimesDefault:
  135. illegalTags.CopyAndAddToTail( "respawntimes" );
  136. illegalTags.CopyAndAddToTail( "norespawntime" );
  137. break;
  138. case QuickplaySearchOptions::eRespawnTimesInstant:
  139. requiredTags.CopyAndAddToTail( "norespawntime" );
  140. break;
  141. default:
  142. Assert( false );
  143. case QuickplaySearchOptions::eRespawnTimesDontCare:
  144. break;
  145. }
  146. switch ( opt.m_eRandomCrits )
  147. {
  148. case QuickplaySearchOptions::eRandomCritsYes:
  149. illegalTags.CopyAndAddToTail( "nocrits" );
  150. break;
  151. case QuickplaySearchOptions::eRandomCritsNo:
  152. requiredTags.CopyAndAddToTail( "nocrits" );
  153. break;
  154. default:
  155. Assert( false );
  156. case QuickplaySearchOptions::eRandomCritsDontCare:
  157. break;
  158. }
  159. switch ( opt.m_eDamageSpread )
  160. {
  161. case QuickplaySearchOptions::eDamageSpreadYes:
  162. requiredTags.CopyAndAddToTail( "dmgspread" );
  163. break;
  164. case QuickplaySearchOptions::eDamageSpreadNo:
  165. illegalTags.CopyAndAddToTail( "dmgspread" );
  166. break;
  167. default:
  168. Assert( false );
  169. case QuickplaySearchOptions::eDamageSpreadDontCare:
  170. break;
  171. }
  172. switch ( opt.m_eMaxPlayers )
  173. {
  174. case QuickplaySearchOptions::eMaxPlayers24:
  175. illegalTags.CopyAndAddToTail( "increased_maxplayers" );
  176. break;
  177. case QuickplaySearchOptions::eMaxPlayers30Plus:
  178. requiredTags.CopyAndAddToTail( "increased_maxplayers" );
  179. break;
  180. default:
  181. Assert( false );
  182. case QuickplaySearchOptions::eMaxPlayersDontCare:
  183. break;
  184. }
  185. switch ( opt.m_eBetaContent )
  186. {
  187. case QuickplaySearchOptions::eBetaYes:
  188. requiredTags.CopyAndAddToTail( "beta" );
  189. break;
  190. case QuickplaySearchOptions::eBetaNo:
  191. illegalTags.CopyAndAddToTail( "beta" );
  192. break;
  193. default:
  194. Assert( false );
  195. break;
  196. }
  197. }
  198. //-----------------------------------------------------------------------------
  199. static void OpenQuickplayDialogCallback( bool bConfirmed, void *pContext )
  200. {
  201. engine->ClientCmd_Unrestricted( "OpenQuickplayDialog" );
  202. }
  203. // Why does it take so many lines of code to do stuff like this?
  204. static CDllDemandLoader g_ServerBrowser( "ServerBrowser" );
  205. static IServerBrowser *GetServerBrowser()
  206. {
  207. static IServerBrowser *pServerBrowser = NULL;
  208. if ( pServerBrowser == NULL )
  209. {
  210. int iReturnCode;
  211. pServerBrowser = (IServerBrowser *)g_ServerBrowser.GetFactory()( SERVERBROWSER_INTERFACE_VERSION, &iReturnCode );
  212. Assert( pServerBrowser );
  213. }
  214. return pServerBrowser;
  215. }
  216. //=============================================================================
  217. enum eGameServerFilter
  218. {
  219. kGameServerListFilter_Internet,
  220. kGameServerListFilter_Favorites,
  221. };
  222. struct sortable_gameserveritem_t
  223. {
  224. sortable_gameserveritem_t()
  225. {
  226. m_bRegistered = false;
  227. m_bValve = false;
  228. m_bNewUserFriendly = false;
  229. m_bMapIsQuickPlayOK = false;
  230. userScore = -999.9f;
  231. serverScore = -999.9f;
  232. m_fRecentMatchPenalty = -999.9f;
  233. m_fTotalScoreFromGC = -9999.9f;
  234. m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_Invalid;
  235. m_nOptionsScoreFromGC = INT_MAX;
  236. }
  237. gameserveritem_t server;
  238. bool m_bRegistered;
  239. bool m_bValve;
  240. bool m_bNewUserFriendly;
  241. bool m_bMapIsQuickPlayOK;
  242. float userScore;
  243. float serverScore;
  244. float m_fRecentMatchPenalty;
  245. float m_fTotalScoreFromGC;
  246. TF_Gamestats_QuickPlay_t::eServerStatus m_eStatus;
  247. int m_nOptionsScoreFromGC;
  248. inline float TotalScore() const { return userScore + serverScore; }
  249. };
  250. // Store a server that has been scored by the GC and
  251. // is ready to receive the final confirmation ping
  252. struct gameserver_ping_queue_entry_t
  253. {
  254. float m_fTotalScore;
  255. netadr_t m_adr;
  256. };
  257. //-----------------------------------------------------------------------------
  258. // Purpose: Score a game server based on number of players and ping
  259. //-----------------------------------------------------------------------------
  260. class CGameServerItemSort
  261. {
  262. public:
  263. bool Less( const sortable_gameserveritem_t *src1, const sortable_gameserveritem_t *src2, void *pCtx )
  264. {
  265. // we want higher scores sorted to the front
  266. return src1->TotalScore() > src2->TotalScore();
  267. }
  268. bool Less( const gameserver_ping_queue_entry_t &src1, const gameserver_ping_queue_entry_t &src2, void *pCtx )
  269. {
  270. // we want higher scores sorted to the front
  271. return src1.m_fTotalScore > src2.m_fTotalScore;
  272. }
  273. };
  274. // !KLUDGE! This is duplicated from serverbrowserQuickListPanel.h
  275. class CQuickListPanel : public vgui::EditablePanel
  276. {
  277. DECLARE_CLASS_SIMPLE( CQuickListPanel, vgui::EditablePanel );
  278. public:
  279. CQuickListPanel( vgui::Panel *parent, const char *panelName );
  280. virtual void ApplySchemeSettings(vgui::IScheme *pScheme);
  281. void SetMapName( const char *pMapName );
  282. void SetImage( const char *pMapName );
  283. void SetGameType( const char *pGameType );
  284. const char *GetMapName( void ) { return m_szMapName; }
  285. void SetRefreshing( void );
  286. virtual void OnMousePressed( vgui::MouseCode code );
  287. virtual void OnMouseDoublePressed( vgui::MouseCode code );
  288. void SetServerInfo ( KeyValues *pKV, int iListID, int iTotalServers );
  289. int GetListID( void ) { return m_iListID; }
  290. virtual void PerformLayout( void )
  291. {
  292. BaseClass::PerformLayout();
  293. }
  294. MESSAGE_FUNC_INT( OnPanelSelected, "PanelSelected", state )
  295. {
  296. if ( state )
  297. {
  298. vgui::IScheme *pScheme = vgui::scheme()->GetIScheme( GetScheme() );
  299. if ( pScheme && m_pBGroundPanel )
  300. {
  301. m_pBGroundPanel->SetBgColor( pScheme->GetColor("QuickListBGSelected", Color(255, 255, 255, 0 ) ) );
  302. }
  303. }
  304. else
  305. {
  306. vgui::IScheme *pScheme = vgui::scheme()->GetIScheme( GetScheme() );
  307. if ( pScheme && m_pBGroundPanel )
  308. {
  309. m_pBGroundPanel->SetBgColor( pScheme->GetColor("QuickListBGDeselected", Color(255, 255, 255, 0 ) ) );
  310. }
  311. }
  312. PostMessage( GetParent()->GetVParent(), new KeyValues("PanelSelected") );
  313. }
  314. private:
  315. char m_szMapName[64];
  316. vgui::ImagePanel *m_pLatencyImage;
  317. vgui::Label *m_pLatencyLabel;
  318. vgui::Label *m_pPlayerCountLabel;
  319. vgui::Label *m_pOtherServersLabel;
  320. vgui::Label *m_pServerNameLabel;
  321. vgui::Panel *m_pBGroundPanel;
  322. vgui::ImagePanel *m_pMapImage;
  323. vgui::Panel *m_pListPanelParent;
  324. vgui::Label *m_pGameTypeLabel;
  325. vgui::Label *m_pMapNameLabel;
  326. vgui::ImagePanel *m_pReplayImage;
  327. int m_iListID;
  328. };
  329. //-----------------------------------------------------------------------------
  330. // Purpose:
  331. //-----------------------------------------------------------------------------
  332. CQuickListPanel::CQuickListPanel( vgui::Panel* pParent, const char *pElementName ) : BaseClass( pParent, pElementName )
  333. {
  334. SetParent( pParent );
  335. m_pListPanelParent = pParent;
  336. SetScheme( "SourceScheme" );
  337. SetProportional( false );
  338. CMouseMessageForwardingPanel *panel = new CMouseMessageForwardingPanel(this, NULL);
  339. panel->SetZPos(3);
  340. m_pLatencyImage = new ImagePanel( this, "latencyimage" );
  341. m_pPlayerCountLabel = new Label( this, "playercount", "" );
  342. m_pOtherServersLabel = new Label( this, "otherservercount", "" );
  343. m_pServerNameLabel = new Label( this, "servername", "" );
  344. m_pBGroundPanel = new Panel( this, "background" );
  345. m_pMapImage = new ImagePanel( this, "mapimage" );
  346. m_pGameTypeLabel = new Label( this, "gametype", "" );
  347. m_pMapNameLabel = new Label( this, "mapname", "" );
  348. m_pLatencyLabel = new Label( this, "latencytext", "" );
  349. m_pReplayImage = new ImagePanel( this, "replayimage" );
  350. const char *pPathID = "PLATFORM";
  351. if ( g_pFullFileSystem->FileExists( "Servers/QuickListPanel.res", "MOD" ) )
  352. {
  353. pPathID = "MOD";
  354. }
  355. LoadControlSettings( "Servers/QuickListPanel.res", pPathID );
  356. }
  357. //-----------------------------------------------------------------------------
  358. // Purpose:
  359. //-----------------------------------------------------------------------------
  360. void CQuickListPanel::ApplySchemeSettings(IScheme *pScheme)
  361. {
  362. BaseClass::ApplySchemeSettings(pScheme);
  363. if ( pScheme && m_pBGroundPanel )
  364. {
  365. m_pBGroundPanel->SetBgColor( pScheme->GetColor("QuickListBGDeselected", Color(255, 255, 255, 0 ) ) );
  366. }
  367. }
  368. //-----------------------------------------------------------------------------
  369. // Purpose:
  370. //-----------------------------------------------------------------------------
  371. void CQuickListPanel::SetRefreshing( void )
  372. {
  373. if ( m_pServerNameLabel )
  374. {
  375. m_pServerNameLabel->SetText( g_pVGuiLocalize->Find("#ServerBrowser_QuickListRefreshing") );
  376. }
  377. if ( m_pPlayerCountLabel )
  378. {
  379. m_pPlayerCountLabel->SetVisible( false );
  380. }
  381. if ( m_pOtherServersLabel )
  382. {
  383. m_pOtherServersLabel->SetVisible( false );
  384. }
  385. if ( m_pLatencyImage )
  386. {
  387. m_pLatencyImage->SetVisible( false );
  388. }
  389. if ( m_pReplayImage )
  390. {
  391. m_pReplayImage->SetVisible( false );
  392. }
  393. if ( m_pLatencyLabel )
  394. {
  395. m_pLatencyLabel->SetVisible( false );
  396. }
  397. }
  398. //-----------------------------------------------------------------------------
  399. // Purpose:
  400. //-----------------------------------------------------------------------------
  401. void CQuickListPanel::SetMapName( const char *pMapName )
  402. {
  403. Q_strncpy( m_szMapName, pMapName, sizeof( m_szMapName ) );
  404. if ( m_pMapNameLabel )
  405. {
  406. m_pMapNameLabel->SetText( pMapName );
  407. m_pMapNameLabel->SizeToContents();
  408. }
  409. }
  410. //-----------------------------------------------------------------------------
  411. // Purpose:
  412. //-----------------------------------------------------------------------------
  413. void CQuickListPanel::SetGameType( const char *pGameType )
  414. {
  415. if ( strlen ( pGameType ) == 0 )
  416. {
  417. m_pGameTypeLabel->SetVisible( false );
  418. return;
  419. }
  420. char gametype[ 512 ];
  421. Q_snprintf( gametype, sizeof( gametype ), "(%s)", pGameType );
  422. m_pGameTypeLabel->SetText( gametype );
  423. }
  424. //-----------------------------------------------------------------------------
  425. // Purpose:
  426. //-----------------------------------------------------------------------------
  427. void CQuickListPanel::SetServerInfo ( KeyValues *pKV, int iListID, int iTotalServers )
  428. {
  429. if ( pKV == NULL )
  430. return;
  431. m_iListID = iListID;
  432. m_pServerNameLabel->SetText( pKV->GetString( "name", " " ) );
  433. int iPing = pKV->GetInt( "ping", 0 );
  434. if ( iPing <= 100 )
  435. {
  436. m_pLatencyImage->SetImage( "../vgui/icon_con_high.vmt" );
  437. }
  438. else if ( iPing <= 150 )
  439. {
  440. m_pLatencyImage->SetImage( "../vgui/icon_con_medium.vmt" );
  441. }
  442. else
  443. {
  444. m_pLatencyImage->SetImage( "../vgui/icon_con_low.vmt" );
  445. }
  446. m_pLatencyImage->SetVisible( false );
  447. //if ( GameSupportsReplay() )
  448. {
  449. m_pReplayImage->SetVisible( pKV->GetInt( "Replay", 0 ) > 0 );
  450. }
  451. char ping[ 512 ];
  452. Q_snprintf( ping, sizeof( ping ), "%d ms", iPing );
  453. m_pLatencyLabel->SetText( ping );
  454. m_pLatencyLabel->SetVisible( true );
  455. wchar_t players[ 512 ];
  456. wchar_t playercount[16];
  457. wchar_t *pwszPlayers = g_pVGuiLocalize->Find("#ServerBrowser_Players");
  458. g_pVGuiLocalize->ConvertANSIToUnicode( pKV->GetString( "players", " " ), playercount, sizeof( playercount ) );
  459. _snwprintf( players, ARRAYSIZE( players ), L"%ls %ls", playercount, pwszPlayers );
  460. m_pPlayerCountLabel->SetText( players );
  461. m_pPlayerCountLabel->SetVisible( true );
  462. // Now setup the other server count
  463. if ( iTotalServers == 2 )
  464. {
  465. m_pOtherServersLabel->SetText( g_pVGuiLocalize->Find("#ServerBrowser_QuickListOtherServer") );
  466. m_pOtherServersLabel->SetVisible( true );
  467. }
  468. else if ( iTotalServers > 2 )
  469. {
  470. wchar_t *pwszServers = g_pVGuiLocalize->Find("#ServerBrowser_QuickListOtherServers");
  471. _snwprintf( playercount, Q_ARRAYSIZE(playercount), L"%d", (iTotalServers-1) );
  472. g_pVGuiLocalize->ConstructString_safe( players, pwszServers, 1, playercount );
  473. m_pOtherServersLabel->SetText( players );
  474. m_pOtherServersLabel->SetVisible( true );
  475. }
  476. else
  477. {
  478. m_pOtherServersLabel->SetVisible( false );
  479. }
  480. }
  481. //-----------------------------------------------------------------------------
  482. // Purpose:
  483. //-----------------------------------------------------------------------------
  484. void CQuickListPanel::SetImage( const char *pMapName )
  485. {
  486. char path[ 512 ];
  487. Q_snprintf( path, sizeof( path ), "materials/vgui/maps/menu_thumb_%s.vmt", pMapName );
  488. char map[ 512 ];
  489. Q_snprintf( map, sizeof( map ), "maps/%s.bsp", pMapName );
  490. if ( g_pFullFileSystem->FileExists( map, "MOD" ) == false )
  491. {
  492. pMapName = "default_download";
  493. }
  494. else
  495. {
  496. if ( g_pFullFileSystem->FileExists( path, "MOD" ) == false )
  497. {
  498. pMapName = "default";
  499. }
  500. }
  501. if ( m_pMapImage )
  502. {
  503. char imagename[ 512 ];
  504. Q_snprintf( imagename, sizeof( imagename ), "..\\vgui\\maps\\menu_thumb_%s", pMapName );
  505. m_pMapImage->SetImage ( imagename );
  506. m_pMapImage->SetMouseInputEnabled( false );
  507. }
  508. }
  509. void CQuickListPanel::OnMousePressed( vgui::MouseCode code )
  510. {
  511. if ( m_pListPanelParent )
  512. {
  513. vgui::PanelListPanel *pParent = dynamic_cast < vgui::PanelListPanel *> ( m_pListPanelParent );
  514. if ( pParent )
  515. {
  516. pParent->SetSelectedPanel( this );
  517. m_pListPanelParent->CallParentFunction( new KeyValues("ItemSelected", "itemID", -1 ) );
  518. }
  519. if ( code == MOUSE_RIGHT )
  520. {
  521. m_pListPanelParent->CallParentFunction( new KeyValues("OpenContextMenu", "itemID", -1 ) );
  522. }
  523. }
  524. }
  525. void CQuickListPanel::OnMouseDoublePressed( vgui::MouseCode code )
  526. {
  527. if ( code == MOUSE_RIGHT )
  528. return;
  529. // call the panel
  530. OnMousePressed( code );
  531. m_pListPanelParent->CallParentFunction( new KeyValues("ConnectToServer", "code", code) );
  532. }
  533. CUtlString GetCommaDelimited( const CUtlStringList &list )
  534. {
  535. CUtlString sResult;
  536. FOR_EACH_VEC( list, idx )
  537. {
  538. if ( idx != 0 )
  539. sResult.Append( "," );
  540. sResult.Append( list[idx] );
  541. }
  542. return sResult;
  543. }
  544. class CQuickplayWaitDialog;
  545. static CQuickplayWaitDialog *s_pQuickPlayWaitingDialog;
  546. // Busy indicator and also server list if the didn't use "I'm feeling lucky"
  547. class CQuickplayWaitDialog : public CGenericWaitingDialog, public ISteamMatchmakingServerListResponse, public ISteamMatchmakingPingResponse
  548. {
  549. DECLARE_CLASS_SIMPLE( CQuickplayWaitDialog, CGenericWaitingDialog );
  550. public:
  551. CQuickplayWaitDialog( bool bFeelingLucky, const QuickplaySearchOptions &opt )
  552. : CGenericWaitingDialog( NULL )
  553. , m_options( opt )
  554. , m_fHoursPlayed( 0 )
  555. , m_nAppID( GetTfMatchmakingAppID() )
  556. , m_hServerListRequest( NULL )
  557. , m_hServerQueryRequest( HSERVERQUERY_INVALID )
  558. , m_fPingA( tf_matchmaking_ping_a.GetFloat() )
  559. , m_fPingAScore( tf_matchmaking_ping_a_score.GetFloat() )
  560. , m_fPingB( tf_matchmaking_ping_b.GetFloat() )
  561. , m_fPingBScore( tf_matchmaking_ping_b_score.GetFloat() )
  562. , m_fPingC( tf_matchmaking_ping_c.GetFloat() )
  563. , m_fPingCScore( tf_matchmaking_ping_c_score.GetFloat() )
  564. , m_bFeelingLucky( bFeelingLucky )
  565. , m_eCurrentStep( k_EStep_GMSQuery )
  566. , m_timeGCScoreTimeout( 0.0 )
  567. , m_timeGMSSearchStarted( 0.0 )
  568. , m_timePingServerTimeout( 0.0 )
  569. , m_pBusyContainer( NULL )
  570. , m_pResultsContainer( NULL )
  571. , m_pServerListPanel( NULL )
  572. , m_pProgressBar( NULL )
  573. , m_ScoringNumbers( steamapicontext ? steamapicontext->SteamUser()->GetSteamID() : CSteamID() )
  574. {
  575. // Check the panel name since we are changing the resource
  576. SetName("QuickPlayBusyDialog");
  577. Assert( s_pQuickPlayWaitingDialog == NULL );
  578. s_pQuickPlayWaitingDialog = this;
  579. m_blackList.LoadServersFromFile( BLACKLIST_DEFAULT_SAVE_FILE, false );
  580. m_fHoursPlayed = CTFStatPanel::GetTotalHoursPlayed();
  581. m_Stats.m_fUserHoursPlayed = m_fHoursPlayed;
  582. m_Stats.m_iExperimentGroup = m_ScoringNumbers.m_eExperimentGroup;
  583. // Determine bonus we'll give to valve servers, using linear
  584. // interpolation with clamped endpoints
  585. float m_fValveBonusHrsA = tf_matchmaking_numbers_valve_bonus_hrs_a.GetFloat();
  586. float m_fValveBonusPtsA = tf_matchmaking_numbers_valve_bonus_pts_a.GetFloat();
  587. float m_fValveBonusHrsB = tf_matchmaking_numbers_valve_bonus_hrs_b.GetFloat();
  588. float m_fValveBonusPtsB = tf_matchmaking_numbers_valve_bonus_pts_b.GetFloat();
  589. Assert( m_fValveBonusHrsA < m_fValveBonusHrsB );
  590. if ( m_fHoursPlayed < m_fValveBonusHrsA )
  591. {
  592. m_fValveBonus = m_fValveBonusPtsA;
  593. }
  594. else if ( m_fHoursPlayed < m_fValveBonusHrsB )
  595. {
  596. m_fValveBonus = lerp( m_fValveBonusHrsA, m_fValveBonusPtsA, m_fValveBonusHrsB, m_fValveBonusPtsB, m_fHoursPlayed );
  597. }
  598. else
  599. {
  600. m_fValveBonus = m_fValveBonusPtsB;
  601. }
  602. // Calc bonus given if the map is noob friendly
  603. float fNoobTime = tf_matchmaking_noob_hours_played.GetFloat();
  604. m_fNoobMapBonus = 0.0f;
  605. if ( m_fHoursPlayed < fNoobTime )
  606. {
  607. float fNoobPct = 1.0f - ( fNoobTime / fNoobTime );
  608. m_fNoobMapBonus = fNoobPct * tf_matchmaking_noob_map_score_boost.GetFloat();
  609. }
  610. switch ( m_options.m_eSelectedGameType )
  611. {
  612. case kGameCategory_Quickplay: m_Stats.m_sUserGameMode = "(any)"; break;
  613. case kGameCategory_Escort: m_Stats.m_sUserGameMode = "escort"; break;
  614. case kGameCategory_CTF: m_Stats.m_sUserGameMode = "ctf"; break;
  615. case kGameCategory_AttackDefense: m_Stats.m_sUserGameMode = "attackdefense"; break;
  616. case kGameCategory_Koth: m_Stats.m_sUserGameMode = "koth"; break;
  617. case kGameCategory_CP: m_Stats.m_sUserGameMode = "cp"; break;
  618. case kGameCategory_EscortRace: m_Stats.m_sUserGameMode = "escortrace"; break;
  619. case kGameCategory_EventMix: m_Stats.m_sUserGameMode = "eventmix"; break;
  620. case kGameCategory_Event247: m_Stats.m_sUserGameMode = "event247"; break;
  621. case kGameCategory_SD: m_Stats.m_sUserGameMode = "sd"; break;
  622. case kGameCategory_RobotDestruction: m_Stats.m_sUserGameMode = "rd"; break;
  623. case kGameCategory_Powerup: m_Stats.m_sUserGameMode = "powerup"; break;
  624. case kGameCategory_Featured: m_Stats.m_sUserGameMode = "featured"; break;
  625. case kGameCategory_Passtime: m_Stats.m_sUserGameMode = "passtime"; break;
  626. case kGameCategory_Community_Update: m_Stats.m_sUserGameMode = "community_update"; break;
  627. case kGameCategory_Misc: m_Stats.m_sUserGameMode = "misc"; break;
  628. //case kQuickplayGameType_Arena: stats.m_sUserGameMode = "arena"; break;
  629. //case kQuickplayGameType_Specialty: stats.m_sUserGameMode = "specialty"; break;
  630. default:
  631. Assert( false );
  632. m_Stats.m_sUserGameMode.Format( "%d", m_options.m_eSelectedGameType );
  633. break;
  634. }
  635. SetDefLessFunc( m_mapServers );
  636. // Sanity
  637. Assert( 0.0 < m_fPingA ); Assert( m_fPingA < m_fPingB ); Assert( m_fPingB < m_fPingC );
  638. Assert( 1.0 > m_fPingAScore ); Assert( m_fPingAScore > m_fPingBScore ); Assert( m_fPingBScore > m_fPingCScore );
  639. // Setup search filters
  640. CUtlVector<MatchMakingKeyValuePair_t> vecServerFilters;
  641. AddFilter( vecServerFilters, "gamedir", COM_GetModDirectory() );
  642. if ( GetUniverse() == k_EUniversePublic )
  643. {
  644. AddFilter( vecServerFilters, "secure", "1" );
  645. AddFilter( vecServerFilters, "dedicated", "1" );
  646. }
  647. AddFilter( vecServerFilters, "full", "1" ); // actually means "not full"
  648. GetQuickplayTags( m_options, m_vecStrRequiredTags, m_vecStrRejectTags );
  649. // Specified map filter if one is specified
  650. if ( !m_options.m_strMapName.IsEmpty() )
  651. {
  652. AddFilter( vecServerFilters, "map", m_options.m_strMapName );
  653. }
  654. else
  655. {
  656. // Add filter for the game mode tag
  657. switch ( m_options.m_eSelectedGameType )
  658. {
  659. case kGameCategory_Escort:
  660. case kGameCategory_EscortRace:
  661. m_vecStrRequiredTags.CopyAndAddToTail( "payload" );
  662. AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType );
  663. break;
  664. case kGameCategory_Koth:
  665. case kGameCategory_AttackDefense:
  666. case kGameCategory_CP:
  667. m_vecStrRequiredTags.CopyAndAddToTail( "cp" );
  668. AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType );
  669. break;
  670. case kGameCategory_CTF:
  671. m_vecStrRequiredTags.CopyAndAddToTail( "ctf" );
  672. AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType );
  673. break;
  674. case kGameCategory_EventMix:
  675. m_vecStrRequiredTags.CopyAndAddToTail( "eventmix" );
  676. m_vecStrRejectTags.CopyAndAddToTail( "event247" );
  677. AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType );
  678. break;
  679. case kGameCategory_Event247:
  680. m_vecStrRequiredTags.CopyAndAddToTail( "event247" );
  681. m_vecStrRejectTags.CopyAndAddToTail( "eventmix" );
  682. AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType );
  683. break;
  684. case kGameCategory_SD:
  685. m_vecStrRequiredTags.CopyAndAddToTail( "sd" );
  686. AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType );
  687. break;
  688. case kGameCategory_RobotDestruction:
  689. m_vecStrRequiredTags.CopyAndAddToTail( "rd" );
  690. AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType );
  691. break;
  692. case kGameCategory_Powerup:
  693. m_vecStrRequiredTags.CopyAndAddToTail( "powerup" );
  694. AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType );
  695. break;
  696. case kGameCategory_Featured:
  697. m_vecStrRequiredTags.CopyAndAddToTail( "featured" );
  698. AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType );
  699. break;
  700. case kGameCategory_Passtime:
  701. m_vecStrRequiredTags.CopyAndAddToTail( "passtime" );
  702. AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType );
  703. break;
  704. case kGameCategory_Community_Update:
  705. m_vecStrRequiredTags.CopyAndAddToTail( "community_update" );
  706. AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType );
  707. break;
  708. case kGameCategory_Misc:
  709. m_vecStrRequiredTags.CopyAndAddToTail( "misc" );
  710. AddMapsFilter( vecServerFilters, m_options.m_eSelectedGameType );
  711. break;
  712. case kGameCategory_Quickplay:
  713. // Grrrrr. Anything we can do here?
  714. // The list of all the maps won't fit. We do not yet have an "or" syntax.
  715. // So we're stuck filtering them client side
  716. break;
  717. default:
  718. Assert( false );
  719. break;
  720. }
  721. }
  722. AddFilter( vecServerFilters, "ngametype", GetCommaDelimited( m_vecStrRejectTags ) );
  723. AddFilter( vecServerFilters, "gametype", GetCommaDelimited( m_vecStrRequiredTags ) );
  724. AddFilter( vecServerFilters, "steamblocking", "1" );
  725. if ( m_options.m_eServers == QuickplaySearchOptions::eServersOfficial )
  726. {
  727. AddFilter( vecServerFilters, "white", "1" );
  728. }
  729. else if ( m_options.m_eServers == QuickplaySearchOptions::eServersCommunity )
  730. {
  731. // community servers *ONLY*
  732. AddFilter( vecServerFilters, "nor", "1" );
  733. AddFilter( vecServerFilters, "white", "1" );
  734. }
  735. // Remember when we started
  736. m_timeGMSSearchStarted = Plat_FloatTime();
  737. // Print for debugging
  738. if ( tf_matchmaking_debug.GetInt() >= 2 )
  739. {
  740. CUtlString sFilter;
  741. FOR_EACH_VEC( vecServerFilters, idx )
  742. {
  743. sFilter.Append( vecServerFilters[idx].m_szKey );
  744. sFilter.Append( " - " );
  745. sFilter.Append( vecServerFilters[idx].m_szValue );
  746. sFilter.Append( "\n" );
  747. }
  748. Msg( "Using GMS filter: %s\n", sFilter.String() );
  749. }
  750. // Initiate the search
  751. MatchMakingKeyValuePair_t *pFilters = vecServerFilters.Base(); // <<<< Note, this is weird, but correct.
  752. m_hServerListRequest = steamapicontext->SteamMatchmakingServers()->RequestInternetServerList( GetTfMatchmakingAppID(), &pFilters, vecServerFilters.Count(), this );
  753. }
  754. virtual ~CQuickplayWaitDialog()
  755. {
  756. Assert( s_pQuickPlayWaitingDialog == this );
  757. s_pQuickPlayWaitingDialog = NULL;
  758. DestroyServerQueryRequest();
  759. DestroyServerListRequest();
  760. }
  761. virtual const char* GetResFile() const { return "Resource/UI/QuickPlayBusyDialog.res"; }
  762. void OnReceivedGCScores( CMsgTFQuickplay_ScoreServersResponse &msg )
  763. {
  764. // Make sure we're expecting them
  765. if ( m_eCurrentStep != k_EStep_GCScore )
  766. {
  767. Warning(" Received CGCTFQuickplay_ScoreServers_Response, but not expecting them (current step = %d)?\n", m_eCurrentStep );
  768. return;
  769. }
  770. m_vecServerJoinQueue.RemoveAll();
  771. // Do we have any GC scores?
  772. if ( msg.servers_size() > 0 )
  773. {
  774. TF_MATCHMAKING_SPEW( 1, "Received %d server scores from GC\n", msg.servers_size());
  775. // Put them in a queue. We will try them in score order
  776. TF_MATCHMAKING_SPEW( 2, "SteamID, IP, score\n" );
  777. for ( int i = 0; i < msg.servers_size(); ++i )
  778. {
  779. const CMsgTFQuickplay_ScoreServersResponse_ServerInfo &info = msg.servers( i );
  780. netadr_t adr( info.server_address(), info.server_port() );
  781. CSteamID steamID( info.steam_id() );
  782. TF_MATCHMAKING_SPEW( 2, "\"%s\", \"%s\", %.2f\n",
  783. steamID.Render(), adr.ToString(), info.total_score() );
  784. gameserver_ping_queue_entry_t item;
  785. item.m_adr.SetIPAndPort( info.server_address(), info.server_port() );
  786. item.m_fTotalScore = info.total_score();
  787. int nOptionsScore = RemapValClamped( info.options_score(), 0.f, 30000.f, tf_mm_options_bonus.GetFloat(), tf_mm_options_penalty.GetFloat() );
  788. item.m_fTotalScore += nOptionsScore;
  789. m_vecServerJoinQueue.InsertNoSort( item );
  790. // Locate the original entry. Try by IP first,
  791. // and if that fails, try steam ID. (Can happen due
  792. // to mismatch of public/private IP)
  793. int idx = m_mapServers.Find( adr );
  794. if ( m_mapServers.IsValidIndex( idx ) )
  795. {
  796. // Might actually fail in race condition, but should
  797. // be extremely rare. If it's happening frequently,
  798. // we probably have a bug or have misunderstood something
  799. Assert( m_mapServers[idx].server.m_steamID == steamID );
  800. }
  801. else
  802. {
  803. bool bFound = false;
  804. FOR_EACH_MAP_FAST( m_mapServers, j )
  805. {
  806. if ( m_mapServers[j].server.m_steamID == steamID )
  807. {
  808. Assert( !m_mapServers.IsValidIndex( idx ) ); // duplicate?
  809. idx = j;
  810. bFound = true;
  811. break;
  812. }
  813. }
  814. if ( bFound )
  815. {
  816. // Remove it and re-insert, updating the address
  817. // we got from the GC, which is probably the public IP.
  818. // this IP is more useful for scoring purposes
  819. sortable_gameserveritem_t temp = m_mapServers[idx];
  820. temp.server.m_NetAdr.SetIP( info.server_address() );
  821. m_mapServers.RemoveAt( idx );
  822. idx = m_mapServers.InsertOrReplace( adr, temp );
  823. }
  824. }
  825. if ( m_mapServers.IsValidIndex( idx ) )
  826. {
  827. m_mapServers[ idx ].m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_Scored;
  828. m_mapServers[ idx ].m_fTotalScoreFromGC = info.total_score();
  829. m_mapServers[ idx ].m_nOptionsScoreFromGC = info.options_score();
  830. }
  831. else
  832. {
  833. Warning( "Received score from GC for %s @ %s, but I didn't ask for that server!\n", steamID.Render(), adr.ToString() );
  834. Assert( m_mapServers.IsValidIndex( idx ) );
  835. }
  836. }
  837. } else {
  838. TF_MATCHMAKING_SPEW(1, "Using client-only scoring (no reputation data)\n" );
  839. // Just add all of the servers that we thought were good enough to send to the GC
  840. // to even request a score into the list of possible ones to join
  841. FOR_EACH_MAP( m_mapServers, i )
  842. {
  843. sortable_gameserveritem_t &s = m_mapServers[i];
  844. if ( s.m_eStatus >= TF_Gamestats_QuickPlay_t::k_Server_RequestedScore )
  845. {
  846. gameserver_ping_queue_entry_t item;
  847. item.m_adr.SetIPAndPort( s.server.m_NetAdr.GetIP(), s.server.m_NetAdr.GetConnectionPort() );
  848. item.m_fTotalScore = s.TotalScore(); // client-side score only, no reputation data
  849. m_vecServerJoinQueue.InsertNoSort( item );
  850. }
  851. }
  852. }
  853. m_vecServerJoinQueue.RedoSort();
  854. if ( msg.servers_size() > 0 )
  855. {
  856. TF_MATCHMAKING_SPEW(1, "Best GC score was %.2f\n", m_vecServerJoinQueue[0].m_fTotalScore );
  857. }
  858. // Show some UI
  859. if ( !m_bFeelingLucky )
  860. {
  861. ShowSelectServerUI();
  862. return;
  863. }
  864. // OK, we have some candidates, but let's only try the first few.
  865. // If they all fail, we probably have some bigger problem.
  866. // !GAH! CUtlVector SetCount will not truncate an existing list,
  867. // it will always fill the list with N dummy entries. I have
  868. // no idea why.
  869. while ( m_vecServerJoinQueue.Count() > 5 )
  870. {
  871. m_vecServerJoinQueue.Remove( m_vecServerJoinQueue.Count()-1 );
  872. }
  873. // OK, we have a server.
  874. C_CTFGameStats::ImmediateWriteInterfaceEvent( "response(quickplay)", "gc_server_found" );
  875. // // !FIXME! Server pinging not working for some reason.
  876. // // Just try to connect to the best one.
  877. // ConnectToServer( m_vecServerJoinQueue[0].m_adr.GetIP(), m_vecServerJoinQueue[0].m_adr.GetPort() );
  878. // return;
  879. // Allright, begin loop pinging servers in order
  880. // until we find one that is still OK to join.
  881. // Hopefully and usually, the first one will
  882. // still be available. But we sometimes waited
  883. // too long and something happened on that server
  884. // during the search and we need to go with our
  885. // 2nd choice
  886. m_eCurrentStep = k_EStep_PingCheckNotFull;
  887. PingNextBestServer();
  888. }
  889. void ShowSelectServerUI()
  890. {
  891. m_eCurrentStep = k_EStep_SelectServerUI;
  892. if ( m_pBusyContainer )
  893. m_pBusyContainer->SetVisible( false );
  894. if ( m_pResultsContainer )
  895. m_pResultsContainer->SetVisible( true );
  896. m_adrCurrentPing.Clear();
  897. if ( m_pServerListPanel )
  898. {
  899. FOR_EACH_VEC( m_vecServerJoinQueue, idxServer )
  900. {
  901. int idxMap = m_mapServers.Find( m_vecServerJoinQueue[ idxServer ].m_adr );
  902. if ( idxMap < 0 )
  903. continue;
  904. sortable_gameserveritem_t &s = m_mapServers[ idxMap ];
  905. CQuickListPanel *pServerPanel = new CQuickListPanel( m_pServerListPanel, "QuickListPanel" );
  906. char szFriendlyName[MAX_MAP_NAME];
  907. const char *pszFriendlyGameTypeName = GetServerBrowser()->GetMapFriendlyNameAndGameType( s.server.m_szMap, szFriendlyName, sizeof(szFriendlyName) );
  908. pServerPanel->SetName( s.server.m_szMap );
  909. pServerPanel->SetMapName( GetMapDisplayName( s.server.m_szMap ) );
  910. pServerPanel->SetImage( s.server.m_szMap );
  911. pServerPanel->SetGameType( pszFriendlyGameTypeName );
  912. KeyValues *pKV = new KeyValues( "ServerInfo" );
  913. pKV->SetInt( "ping", s.server.m_nPing );
  914. pKV->SetString( "name", s.server.GetName() );
  915. pKV->SetString( "players", CFmtStr( "%d/%d", s.server.m_nPlayers, s.server.m_nMaxPlayers ) );
  916. pServerPanel->SetServerInfo( pKV, idxMap, 0 );
  917. pKV->deleteThis();
  918. pServerPanel->InvalidateLayout();
  919. pServerPanel->SetVisible( true );
  920. m_pServerListPanel->AddItem( NULL, pServerPanel );
  921. // We need to reset the proportional flag again because the AddItem() call does a SetParent()
  922. // call which copies the parent's proportional setting to the panel being added
  923. pServerPanel->SetProportional( false );
  924. }
  925. m_pServerListPanel->InvalidateLayout( false, true );
  926. }
  927. }
  928. void ApplySchemeSettings( vgui::IScheme *pScheme )
  929. {
  930. CGenericWaitingDialog::ApplySchemeSettings( pScheme );
  931. m_pBusyContainer = dynamic_cast< vgui::EditablePanel* >( FindChildByName( "BusyContainer" ) );
  932. m_pResultsContainer = dynamic_cast< vgui::EditablePanel* >( FindChildByName( "ResultsContainer" ) );
  933. if ( m_pBusyContainer == NULL || m_pResultsContainer == NULL )
  934. {
  935. Assert( m_pBusyContainer );
  936. Assert( m_pResultsContainer );
  937. return;
  938. }
  939. m_pBusyContainer->SetVisible( true );
  940. m_pResultsContainer->SetVisible( false );
  941. m_pServerListPanel = dynamic_cast< vgui::PanelListPanel* >( m_pResultsContainer->FindChildByName( "ServerList" ) ); Assert( m_pServerListPanel );
  942. if ( m_pServerListPanel )
  943. m_pServerListPanel->SetFirstColumnWidth( 0 );
  944. m_pProgressBar = dynamic_cast< vgui::ProgressBar* >( m_pBusyContainer->FindChildByName( "Progress" ) ); Assert( m_pProgressBar );
  945. vgui::Label *pTitle = dynamic_cast< vgui::Label* >( m_pBusyContainer->FindChildByName( "TitleLabel" ) ); Assert( pTitle );
  946. if ( pTitle ) pTitle->SetText( m_bFeelingLucky ? "#TF_MM_WaitDialog_Title_FeelingLucky" : "#TF_MM_WaitDialog_Title_ShowServers" );
  947. vgui::Panel *pPanel;
  948. pPanel = m_pBusyContainer->FindChildByName( "CloseButton" ); Assert( pPanel );
  949. if ( pPanel ) pPanel->AddActionSignalTarget( this );
  950. pPanel = m_pResultsContainer->FindChildByName( "ConnectButton" ); Assert( pPanel );
  951. if ( pPanel ) pPanel->AddActionSignalTarget( this );
  952. pPanel = m_pResultsContainer->FindChildByName( "CancelButton" ); Assert( pPanel );
  953. if ( pPanel ) pPanel->AddActionSignalTarget( this );
  954. }
  955. void ConnectToServer( uint32 iAddress, uint16 iPort )
  956. {
  957. SendStats( TF_Gamestats_QuickPlay_t::k_Result_TriedToConnect );
  958. netadr_t address( iAddress, iPort );
  959. Msg("Quickplay connecting to %s\n", address.ToString() );
  960. // Close the window (and queue us for deletion), set our status
  961. CloseMe();
  962. // Find/add an entry in the list of recent servers
  963. int iRecentIndex = FindRecentlyMatchedServer( iAddress, iPort );
  964. if ( iRecentIndex < 0 )
  965. {
  966. iRecentIndex = s_vecRecentlyMatchedServers.AddToTail();
  967. s_vecRecentlyMatchedServers[ iRecentIndex ].m_ip = iAddress;
  968. s_vecRecentlyMatchedServers[ iRecentIndex ].m_port = iPort;
  969. }
  970. // Set time when we were matched to this server
  971. s_vecRecentlyMatchedServers[ iRecentIndex ].m_timeMatched = CRTime::RTime32TimeCur();
  972. const char *pszConnectCode = m_bFeelingLucky ? "quickplay" : "quickpick";
  973. char command[256];
  974. Q_snprintf( command, sizeof(command), "connect %s %s_%d\n", address.ToString(), pszConnectCode, g_iQuickplaySessionIndex );
  975. engine->ClientCmd_Unrestricted( command );
  976. }
  977. void OnCommand( const char *pCommand )
  978. {
  979. if ( FStrEq( pCommand, "ConnectToServer" ) )
  980. {
  981. UserConnectToServer();
  982. return;
  983. }
  984. BaseClass::OnCommand( pCommand );
  985. }
  986. virtual void OnUserClose()
  987. {
  988. SendStats( TF_Gamestats_QuickPlay_t::k_Result_UserCancel );
  989. BaseClass::OnUserClose();
  990. }
  991. protected:
  992. enum EStep
  993. {
  994. k_EStep_GMSQuery, // We're waiting on the GMS to give us back some servers
  995. k_EStep_GCScore, // We're waiting on the GC to send us back scores
  996. k_EStep_SelectServerUI, // User wasn't feeling lucky, wanted to see his options first
  997. k_EStep_PingCheckNotFull, // We're looking for the highest scores server that isn't full
  998. k_EStep_Terminated, // We've been terminated and are waiting to die
  999. };
  1000. EStep m_eCurrentStep;
  1001. bool m_bFeelingLucky;
  1002. CUtlMap< netadr_t, sortable_gameserveritem_t > m_mapServers;
  1003. HServerListRequest m_hServerListRequest;
  1004. HServerQuery m_hServerQueryRequest;
  1005. uint32 m_nAppID;
  1006. CBlacklistedServerManager m_blackList;
  1007. QuickplaySearchOptions m_options;
  1008. CUtlStringList m_vecStrRequiredTags;
  1009. CUtlStringList m_vecStrRejectTags;
  1010. float m_fHoursPlayed;
  1011. double m_timeGCScoreTimeout;
  1012. double m_timeGMSSearchStarted;
  1013. double m_timePingServerTimeout;
  1014. netadr_t m_adrCurrentPing;
  1015. vgui::EditablePanel *m_pBusyContainer;
  1016. vgui::EditablePanel *m_pResultsContainer;
  1017. vgui::PanelListPanel *m_pServerListPanel;
  1018. vgui::ProgressBar *m_pProgressBar;
  1019. // Piecewise linear data points for ping->score function
  1020. float m_fPingA, m_fPingAScore;
  1021. float m_fPingB, m_fPingBScore;
  1022. float m_fPingC, m_fPingCScore;
  1023. float m_fValveBonus;
  1024. float m_fNoobMapBonus;
  1025. // List of scores servers we will try to join (after confirming they are still
  1026. // OK), in order
  1027. CUtlSortVector<gameserver_ping_queue_entry_t, CGameServerItemSort> m_vecServerJoinQueue;
  1028. TF2ScoringNumbers_t m_ScoringNumbers;
  1029. TF_Gamestats_QuickPlay_t m_Stats;
  1030. void SendStats( TF_Gamestats_QuickPlay_t::eResult result )
  1031. {
  1032. // Set final result code
  1033. m_Stats.m_eResultCode = result;
  1034. // Duration of total process
  1035. m_Stats.m_fSearchTime = Plat_FloatTime() - m_timeGMSSearchStarted;
  1036. // Populate the server list
  1037. FOR_EACH_MAP( m_mapServers, i )
  1038. {
  1039. sortable_gameserveritem_t &item = m_mapServers[i];
  1040. TF_Gamestats_QuickPlay_t::Server_t &s = m_Stats.m_vecServers[m_Stats.m_vecServers.AddToTail()];
  1041. Assert( item.m_eStatus != TF_Gamestats_QuickPlay_t::k_Server_Invalid );
  1042. s.m_eStatus = item.m_eStatus;
  1043. s.m_ip = item.server.m_NetAdr.GetIP();
  1044. s.m_port = item.server.m_NetAdr.GetConnectionPort();
  1045. s.m_bSecure = item.server.m_bSecure;
  1046. s.m_bRegistered = item.m_bRegistered;
  1047. s.m_bValve = item.m_bValve;
  1048. s.m_nPlayers = item.server.m_nPlayers;
  1049. s.m_nMaxPlayers = item.server.m_nMaxPlayers;
  1050. s.m_sMapName = item.server.m_szMap;
  1051. s.m_sTags = item.server.m_szGameTags;
  1052. s.m_iPing = item.server.m_nPing;
  1053. s.m_bMapIsQuickPlayOK = s.m_bMapIsQuickPlayOK;
  1054. s.m_bMapIsNewUserFriendly = item.m_bNewUserFriendly;
  1055. s.m_fScoreClient = item.userScore;
  1056. s.m_fScoreServer = item.serverScore;
  1057. s.m_fScoreGC = item.m_fTotalScoreFromGC;
  1058. }
  1059. // Send em
  1060. C_CTF_GameStats.QuickplayResults( m_Stats );
  1061. }
  1062. //
  1063. // ISteamMatchmakingServerListResponse overrides
  1064. //
  1065. virtual void ServerResponded( HServerListRequest hRequest, int iServer )
  1066. {
  1067. Assert( m_eCurrentStep == k_EStep_GMSQuery && m_hServerListRequest );
  1068. gameserveritem_t *server = steamapicontext->SteamMatchmakingServers()->GetServerDetails( hRequest, iServer );
  1069. if ( server )
  1070. {
  1071. ServerResponded( *server );
  1072. }
  1073. }
  1074. virtual void ServerFailedToRespond( HServerListRequest hRequest, int iServer )
  1075. {
  1076. // Should only happen during server list process, in which case we don't
  1077. // care. (We just won't add it to our list.)
  1078. Assert( m_eCurrentStep == k_EStep_GMSQuery && m_hServerListRequest );
  1079. }
  1080. virtual void RefreshComplete( HServerListRequest hRequest, EMatchMakingServerResponse response )
  1081. {
  1082. // Destroy our request. We'll pick up on this fact next time in Think()
  1083. // and send the best servers to the GC to be scored
  1084. Assert( m_eCurrentStep == k_EStep_GMSQuery && m_hServerListRequest );
  1085. DestroyServerListRequest();
  1086. }
  1087. //
  1088. // ISteamMatchmakingPingResponse overrides
  1089. //
  1090. virtual void ServerResponded( gameserveritem_t &server )
  1091. {
  1092. // Ignore servers with bogus addres. Is this possible?
  1093. if ( !server.m_NetAdr.GetIP() || !server.m_NetAdr.GetConnectionPort() )
  1094. {
  1095. Assert( server.m_NetAdr.GetIP() && server.m_NetAdr.GetConnectionPort() );
  1096. return;
  1097. }
  1098. // Bogus steam ID?
  1099. if ( !server.m_steamID.IsValid() || !server.m_steamID.BGameServerAccount() || server.m_steamID.GetAccountID() == 0 )
  1100. {
  1101. TF_MATCHMAKING_SPEW( 2, "Quickplay ignoring gameserver at %s with invalid Steam ID %s", server.m_NetAdr.GetQueryAddressString(), server.m_steamID.Render() );
  1102. return;
  1103. }
  1104. //
  1105. // Filter the server
  1106. //
  1107. sortable_gameserveritem_t item;
  1108. item.server = server;
  1109. item.m_bNewUserFriendly = false;
  1110. // XXX(JohnS): This function is no longer available to clients with the matchmaking re-work. If we start using
  1111. // this UI for valve servers again we will need to fix this somehow.
  1112. // BIsIPInValveAddressSpace( item.server.m_NetAdr.GetIP() );
  1113. item.m_bValve = false;
  1114. // Parse out tags
  1115. CUtlStringList TagList; // auto-deletes strings on scope exit
  1116. if ( item.server.m_szGameTags && item.server.m_szGameTags[0] )
  1117. {
  1118. V_SplitString( item.server.m_szGameTags, ",", TagList );
  1119. }
  1120. // Check if the server is registered
  1121. item.m_bRegistered = BHasTag( TagList, "_registered" );
  1122. const SchemaMap_t *pMapInfo = GetItemSchema()->GetMapForName( item.server.m_szMap );
  1123. if ( pMapInfo != NULL )
  1124. {
  1125. item.m_bMapIsQuickPlayOK = true;
  1126. item.m_bNewUserFriendly = pMapInfo->eQuickplayType == kQuickplay_AllUsers;
  1127. }
  1128. else
  1129. {
  1130. item.m_bMapIsQuickPlayOK = false;
  1131. item.m_bNewUserFriendly = false;
  1132. }
  1133. // Joined recently?
  1134. CalculateRecentMatchPenalty( item );
  1135. // Do some basic filtering. We put an extremely low score in there if the server fails
  1136. // to match the criteria. The actual value isn't important here, but it's handy to have
  1137. // different values for different failure criteria for stats.
  1138. int failureCodes = 0;
  1139. if ( !PassesFilter( item.server ) ) failureCodes |= (1<<15);
  1140. if ( !HasAppropriateTags(TagList) ) failureCodes |= (1<<14);
  1141. if ( pMapInfo == NULL )
  1142. failureCodes |= (1<<13); // unknown map
  1143. else
  1144. {
  1145. // Map is known to us, make sure it matches search criteria
  1146. switch ( m_options.m_eSelectedGameType )
  1147. {
  1148. case kGameCategory_EventMix:
  1149. if ( pMapInfo->eGameCategory != kGameCategory_EventMix && pMapInfo->eGameCategory != kGameCategory_Event247 )
  1150. failureCodes |= (1<<12);
  1151. break;
  1152. case kGameCategory_Quickplay:
  1153. // Any map that's in the list will do
  1154. break;
  1155. default:
  1156. // Must match requested game mode
  1157. if ( pMapInfo->eGameCategory != m_options.m_eSelectedGameType )
  1158. failureCodes |= (1<<12);
  1159. }
  1160. }
  1161. if ( server.m_nPlayers >= server.m_nMaxPlayers ) failureCodes |= (1<<11);
  1162. if ( m_blackList.IsServerBlacklisted( server ) ) failureCodes |= (1<<10);
  1163. if ( failureCodes != 0 )
  1164. {
  1165. item.serverScore = item.userScore = -(float)failureCodes;
  1166. item.m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_Ineligible;
  1167. // Check for certain failures that a valve server should never have
  1168. if ( item.m_bValve && ( failureCodes & ( (1<<15) | (1<<14) | (1<<13) ) ) )
  1169. {
  1170. Warning( "Valve-hosted server '%s' does not meet quickplay criteria?\n", server.GetName() );
  1171. }
  1172. }
  1173. else
  1174. {
  1175. // Server pases basic filters and should be scored
  1176. item.m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_Eligible;
  1177. // Start a score based only on the server, not on us
  1178. int nHumans = item.server.m_nPlayers - item.server.m_nBotPlayers;
  1179. int nNumInSearchParty = 1; // we are a party of 1
  1180. item.serverScore = QuickplayCalculateServerScore( nHumans, item.server.m_nBotPlayers, item.server.m_nMaxPlayers, nNumInSearchParty );
  1181. 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.)
  1182. // Start calculating score based on our own data
  1183. item.userScore = 0.0f;
  1184. // Evaluate piecewise linear ping->score function
  1185. float fPing = (float)item.server.m_nPing;
  1186. float fPingScore = 0.0f;
  1187. if ( fPing < m_fPingA )
  1188. {
  1189. fPingScore = lerp( 0.0f, 1.0f, m_fPingA, m_fPingAScore, fPing );
  1190. }
  1191. else if ( fPing < m_fPingB )
  1192. {
  1193. fPingScore = lerp( m_fPingA, m_fPingAScore, m_fPingB, m_fPingCScore, fPing );
  1194. }
  1195. else
  1196. {
  1197. // note: This continues to get worse and worse if the ping > pingC
  1198. fPingScore = lerp( m_fPingB, m_fPingBScore, m_fPingC, m_fPingCScore, fPing );
  1199. }
  1200. item.userScore += fPingScore;
  1201. // Give a bonus to valve servers
  1202. if ( item.m_bValve )
  1203. {
  1204. item.userScore += m_fValveBonus;
  1205. }
  1206. // weight "newbie" servers a higher
  1207. if ( item.m_bNewUserFriendly )
  1208. {
  1209. item.userScore += m_fNoobMapBonus;
  1210. }
  1211. // Add in recently-joined cooldown factor
  1212. item.userScore -= item.m_fRecentMatchPenalty;
  1213. // Penalty for rule changes
  1214. //item.userScore -= QuickplayCalculateRuleChangePenalty( TagList );
  1215. //
  1216. // Experiment
  1217. //
  1218. switch ( m_ScoringNumbers.m_eExperimentGroup )
  1219. {
  1220. default:
  1221. Assert( !"Bogus experiment group" );
  1222. case TF2ScoringNumbers_t::k_ExperimentGroup_Experiment1_Control:
  1223. case TF2ScoringNumbers_t::k_ExperimentGroup_Experiment1_ValveBiasInactive:
  1224. case TF2ScoringNumbers_t::k_ExperimentGroup_Experiment1_CommunityBiasInactive:
  1225. // No modifications
  1226. break;
  1227. case TF2ScoringNumbers_t::k_ExperimentGroup_Experiment1_ValveBias:
  1228. // Give valve servers with a good ping a significant bonus
  1229. item.userScore -= 1.0f;
  1230. if ( item.m_bValve )
  1231. {
  1232. item.userScore += fPingScore;
  1233. }
  1234. break;
  1235. case TF2ScoringNumbers_t::k_ExperimentGroup_Experiment1_CommunityBias:
  1236. // Give community servers with a good ping a significant bonus
  1237. item.userScore -= 1.0f;
  1238. if ( !item.m_bValve )
  1239. {
  1240. item.userScore += fPingScore;
  1241. }
  1242. break;
  1243. }
  1244. }
  1245. netadr_t netAdr( server.m_NetAdr.GetIP(), server.m_NetAdr.GetConnectionPort() );
  1246. // Are we still in the initial pass, or are we pinging servers?
  1247. if ( m_eCurrentStep == k_EStep_GMSQuery )
  1248. {
  1249. // Insert/update our server map
  1250. m_mapServers.InsertOrReplace( netAdr, item );
  1251. }
  1252. else if ( m_eCurrentStep == k_EStep_PingCheckNotFull || m_eCurrentStep == k_EStep_SelectServerUI )
  1253. {
  1254. // It had better be from who we expect
  1255. if ( netAdr.GetIPNetworkByteOrder() != m_adrCurrentPing.GetIPNetworkByteOrder() || netAdr.GetPort() != m_adrCurrentPing.GetPort() )
  1256. {
  1257. // Of course, we could receive a packet on the network at any
  1258. // time. But we expect the steam API's to deal with that.
  1259. // But still, just in case....
  1260. Warning( "Received unexpected server ping from %s\n", netAdr.ToString() );
  1261. }
  1262. else
  1263. {
  1264. // !TEST! Simulate server filling up
  1265. //failureCodes |= (1<<11);
  1266. // Find it in the server list; update the result
  1267. int iServerIndex = m_mapServers.Find( m_adrCurrentPing );
  1268. Assert( m_mapServers.IsValidIndex(iServerIndex) );
  1269. if ( failureCodes == 0 )
  1270. {
  1271. if ( m_mapServers.IsValidIndex(iServerIndex) )
  1272. {
  1273. Assert( m_mapServers[iServerIndex].m_eStatus == TF_Gamestats_QuickPlay_t::k_Server_Pinged );
  1274. m_mapServers[iServerIndex].m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_Connected;
  1275. }
  1276. // Still joinable. JOIN IT!
  1277. TF_MATCHMAKING_SPEW(2, "Reply ping from %s. Client score is now %.02f.\n", netAdr.ToString(), item.TotalScore() );
  1278. ConnectToServer( netAdr.GetIPHostByteOrder(), netAdr.GetPort() );
  1279. }
  1280. else
  1281. {
  1282. if ( m_mapServers.IsValidIndex(iServerIndex) )
  1283. {
  1284. Assert( m_mapServers[iServerIndex].m_eStatus == TF_Gamestats_QuickPlay_t::k_Server_Pinged );
  1285. m_mapServers[iServerIndex].m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_PingIneligible;
  1286. }
  1287. // We missed the window of opportunity.
  1288. // Move on to our next best choice
  1289. TF_MATCHMAKING_SPEW(2, "Reply ping from %s, but no longer joinable!\n", netAdr.ToString() );
  1290. if ( m_eCurrentStep == k_EStep_SelectServerUI )
  1291. {
  1292. if ( failureCodes & (1<<11) )
  1293. QuickpickPingFailed( "#TF_Quickplay_SelectedServer_Full" );
  1294. else
  1295. QuickpickPingFailed( "#TF_Quickplay_SelectedServer_NoLongerMeetsCriteria" );
  1296. }
  1297. else
  1298. {
  1299. PingNextBestServer();
  1300. }
  1301. }
  1302. }
  1303. }
  1304. else
  1305. {
  1306. // Eh?
  1307. Assert( false );
  1308. }
  1309. }
  1310. virtual void ServerFailedToRespond()
  1311. {
  1312. if ( m_eCurrentStep == k_EStep_PingCheckNotFull || m_eCurrentStep == k_EStep_SelectServerUI )
  1313. {
  1314. TF_MATCHMAKING_SPEW(2, "Timeout waiting on ping reply from %s.\n", m_adrCurrentPing.ToString() );
  1315. // Find it in the server list; record the failure result
  1316. int iServerIndex = m_mapServers.Find( m_adrCurrentPing );
  1317. if ( m_mapServers.IsValidIndex(iServerIndex) )
  1318. {
  1319. Assert( m_mapServers[iServerIndex].m_eStatus == TF_Gamestats_QuickPlay_t::k_Server_Pinged );
  1320. m_mapServers[iServerIndex].m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_PingTimedOut;
  1321. }
  1322. else
  1323. {
  1324. // Wat.
  1325. Assert( m_mapServers.IsValidIndex(iServerIndex) );
  1326. }
  1327. if ( m_eCurrentStep == k_EStep_SelectServerUI )
  1328. {
  1329. QuickpickPingFailed( "#TF_Quickplay_SelectedServer_Timeout" );
  1330. }
  1331. else
  1332. {
  1333. PingNextBestServer();
  1334. }
  1335. }
  1336. else
  1337. {
  1338. // We can get some timeouts after we've finished, during cleanup.
  1339. // Just ignore it
  1340. Assert( m_eCurrentStep == k_EStep_Terminated );
  1341. }
  1342. }
  1343. //
  1344. // Internal members
  1345. //
  1346. bool HasAppropriateTags( const CUtlStringList &TagList )
  1347. {
  1348. FOR_EACH_VEC( m_vecStrRequiredTags, idx )
  1349. {
  1350. if ( !BHasTag( TagList, m_vecStrRequiredTags[idx] ) )
  1351. return false;
  1352. }
  1353. FOR_EACH_VEC( m_vecStrRejectTags, idx )
  1354. {
  1355. if ( BHasTag( TagList, m_vecStrRejectTags[idx] ) )
  1356. return false;
  1357. }
  1358. return true;
  1359. }
  1360. void DestroyServerListRequest()
  1361. {
  1362. if ( m_hServerListRequest )
  1363. {
  1364. steamapicontext->SteamMatchmakingServers()->ReleaseRequest( m_hServerListRequest );
  1365. m_hServerListRequest = NULL;
  1366. }
  1367. }
  1368. void DestroyServerQueryRequest()
  1369. {
  1370. if ( m_hServerQueryRequest != HSERVERQUERY_INVALID )
  1371. {
  1372. steamapicontext->SteamMatchmakingServers()->CancelServerQuery( m_hServerQueryRequest );
  1373. m_hServerQueryRequest = HSERVERQUERY_INVALID;
  1374. }
  1375. }
  1376. static void AddFilter( CUtlVector<MatchMakingKeyValuePair_t> &vecServerFilters, const char *pchKey, const char *pchValue )
  1377. {
  1378. // @note Tom Bui: this is to get around the linker error where we don't like strncpy in MatchMakingKeyValuePair_t's constructor!
  1379. int idx = vecServerFilters.AddToTail();
  1380. MatchMakingKeyValuePair_t &keyvalue = vecServerFilters[idx];
  1381. Q_strncpy( keyvalue.m_szKey, pchKey, sizeof( keyvalue.m_szKey ) );
  1382. Q_strncpy( keyvalue.m_szValue, pchValue, sizeof( keyvalue.m_szValue ) );
  1383. }
  1384. static void AddMapsFilter( CUtlVector<MatchMakingKeyValuePair_t> &vecServerFilters, EGameCategory t )
  1385. {
  1386. CUtlString sMapList;
  1387. for ( int i = 0 ; i < GetItemSchema()->GetMapCount() ; ++i )
  1388. {
  1389. const SchemaMap_t& map = GetItemSchema()->GetMapForIndex( i );
  1390. int mapType = map.eGameCategory;
  1391. if ( ( mapType == t ) || ( ( mapType == kGameCategory_Event247 ) && ( t == kGameCategory_EventMix ) ) )
  1392. {
  1393. if ( !sMapList.IsEmpty() )
  1394. {
  1395. sMapList.Append( "," );
  1396. }
  1397. sMapList.Append( map.pszMapName );
  1398. }
  1399. }
  1400. MatchMakingKeyValuePair_t kludge;
  1401. if ( sMapList.Length() < sizeof( kludge.m_szValue ) )
  1402. {
  1403. AddFilter( vecServerFilters, "map", sMapList );
  1404. }
  1405. else
  1406. {
  1407. Warning( "List of map names too long for this game mode, cannot filter on server side!\n" );
  1408. Assert( false );
  1409. }
  1410. }
  1411. bool PassesFilter( const gameserveritem_t &server )
  1412. {
  1413. if ( server.m_nAppID != m_nAppID )
  1414. {
  1415. // This should be filtered by the steam matchmaking servers API
  1416. AssertMsg( server.m_nAppID == 0, "Got back server with wrong APP ID?" );
  1417. return false;
  1418. }
  1419. if ( server.m_bPassword )
  1420. return false;
  1421. if ( GetUniverse() == k_EUniversePublic )
  1422. {
  1423. if ( server.m_bSecure == false )
  1424. return false;
  1425. }
  1426. if ( server.m_nMaxPlayers > kTFQuickPlayMaxPlayers )
  1427. return false;
  1428. if ( server.m_nMaxPlayers < kTFQuickPlayMinMaxNumberOfPlayers )
  1429. return false;
  1430. switch ( m_options.m_eMaxPlayers )
  1431. {
  1432. case QuickplaySearchOptions::eMaxPlayers24:
  1433. if ( server.m_nMaxPlayers != 24 )
  1434. return false;
  1435. break;
  1436. case QuickplaySearchOptions::eMaxPlayers30Plus:
  1437. if ( server.m_nMaxPlayers < 30 )
  1438. return false;
  1439. break;
  1440. default:
  1441. Assert( false );
  1442. case QuickplaySearchOptions::eMaxPlayersDontCare:
  1443. break;
  1444. }
  1445. if ( server.m_steamID.BGameServerAccount() == false )
  1446. return false;
  1447. return true;
  1448. }
  1449. void CalculateRecentMatchPenalty( sortable_gameserveritem_t &item )
  1450. {
  1451. // Hysteresis
  1452. int iRecentIndex = FindRecentlyMatchedServer( item.server.m_NetAdr.GetIP(), item.server.m_NetAdr.GetConnectionPort() );
  1453. item.m_fRecentMatchPenalty = 0.0f;
  1454. if ( iRecentIndex >= 0 )
  1455. {
  1456. float fAge = (float)(CRTime::RTime32TimeCur() - s_vecRecentlyMatchedServers[iRecentIndex].m_timeMatched);
  1457. if ( fAge < 0.0f )
  1458. {
  1459. // Time running in reverse?!
  1460. Assert( fAge >= 0.0f );
  1461. }
  1462. else
  1463. {
  1464. float fCooldownTime = tf_matchmaking_retry_cooldown_seconds.GetFloat();
  1465. Assert( fCooldownTime > 0.0f );
  1466. 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...
  1467. {
  1468. float fAgePct = MIN( fAge / fCooldownTime, 1.0f );
  1469. item.m_fRecentMatchPenalty = (1.0f - fAgePct) * tf_matchmaking_retry_max_penalty.GetFloat();
  1470. }
  1471. }
  1472. }
  1473. }
  1474. virtual void OnThink()
  1475. {
  1476. BaseClass::OnThink();
  1477. double currentTime = Plat_FloatTime();
  1478. // Do work, depending on the current step
  1479. switch ( m_eCurrentStep )
  1480. {
  1481. case k_EStep_GMSQuery:
  1482. CheckSendScoresToGC();
  1483. break;
  1484. case k_EStep_GCScore:
  1485. // Check for timeout
  1486. Assert( m_timeGCScoreTimeout > 0.0 );
  1487. if ( currentTime > m_timeGCScoreTimeout )
  1488. {
  1489. // Well, we never heard back from the GC. But we needn't let that stop us.
  1490. // We have quite a bit of data, let's just go with what we have, rather
  1491. // than totally failing
  1492. C_CTFGameStats::ImmediateWriteInterfaceEvent( "response(quickplay)", "gc_score_timeout" );
  1493. Warning( "Timed out waiting to get scores back from GC, proceding without reputation data.\n" );
  1494. CMsgTFQuickplay_ScoreServersResponse emptyMsg;
  1495. OnReceivedGCScores( emptyMsg );
  1496. }
  1497. break;
  1498. case k_EStep_SelectServerUI:
  1499. // !FIXME! Ping servers every now and then and remove the full ones?
  1500. break;
  1501. case k_EStep_PingCheckNotFull:
  1502. // Do our own timeout processing
  1503. Assert( m_hServerQueryRequest != HSERVERQUERY_INVALID );
  1504. if ( currentTime > m_timePingServerTimeout || m_hServerQueryRequest == HSERVERQUERY_INVALID )
  1505. {
  1506. ServerFailedToRespond();
  1507. }
  1508. break;
  1509. case k_EStep_Terminated:
  1510. // Just wait to die
  1511. break;
  1512. default:
  1513. Assert( false );
  1514. SendStats( TF_Gamestats_QuickPlay_t::k_Result_InternalError );
  1515. GenericFailure();
  1516. break;
  1517. }
  1518. }
  1519. // Called when we're done, just before we go on to wherever we're going next
  1520. void CloseMe()
  1521. {
  1522. m_eCurrentStep = k_EStep_Terminated;
  1523. DestroyServerListRequest();
  1524. DestroyServerQueryRequest();
  1525. CloseWaitingDialog();
  1526. }
  1527. virtual void GenericFailure()
  1528. {
  1529. CloseMe();
  1530. ShowMessageBox( "#TF_MM_GenericFailure_Title", "#TF_MM_GenericFailure", "#GameUI_OK", &OpenQuickplayDialogCallback );
  1531. }
  1532. virtual void NoServersFoundFailure()
  1533. {
  1534. CloseMe();
  1535. ShowMessageBox( "#TF_MM_ResultsDialog_Title", "#TF_MM_ResultsDialog_ServerNotFound", "#GameUI_OK", &OpenQuickplayDialogCallback );
  1536. }
  1537. void CheckSendScoresToGC()
  1538. {
  1539. // How long have we been searching?
  1540. Assert( m_timeGMSSearchStarted > 0.0 );
  1541. float fSearchDuration = Plat_FloatTime() - m_timeGMSSearchStarted;
  1542. // Get a list of all the servers worth joining, and sort them
  1543. float fScoreThreshold = 1.0f; // !FIXME! Put into convar
  1544. CUtlSortVector< sortable_gameserveritem_t *, CGameServerItemSort > vecGameServers;
  1545. FOR_EACH_MAP_FAST( m_mapServers, idx )
  1546. {
  1547. sortable_gameserveritem_t *pItem = &m_mapServers[idx];
  1548. if ( pItem->TotalScore() > fScoreThreshold )
  1549. {
  1550. vecGameServers.InsertNoSort( pItem );
  1551. }
  1552. }
  1553. vecGameServers.RedoSort();
  1554. // See how long we've been searching, relative to the max time
  1555. Assert( tf_matchmaking_max_search_time.GetFloat() > 0.0 ); // don't be dumb
  1556. float fSearchTimePct = fSearchDuration / tf_matchmaking_max_search_time.GetFloat();
  1557. // Give up based on sheer timeout?
  1558. if ( fSearchTimePct > 1.0 )
  1559. {
  1560. // Time to give up
  1561. TF_MATCHMAKING_SPEW(1, "Quickplay GMS search timed out. We'll go with what we've found so far.\n" );
  1562. DestroyServerListRequest();
  1563. }
  1564. // Check if we should keep searching
  1565. if ( m_hServerListRequest )
  1566. {
  1567. // Determine "good enough" tolerances
  1568. float fScoreLo = Lerp( fSearchTimePct, tf_matchmaking_goodenough_score_start.GetFloat(), tf_matchmaking_goodenough_score_end.GetFloat() );
  1569. float fCountLo = Lerp( fSearchTimePct, tf_matchmaking_goodenough_count_start.GetFloat(), tf_matchmaking_goodenough_count_end.GetFloat() );
  1570. //float fScoreHi = Lerp( fSearchTimePct, tf_matchmaking_goodenough_hi_score_start.GetFloat(), tf_matchmaking_goodenough_hi_score_end.GetFloat() );
  1571. //float fCountHi = Lerp( fSearchTimePct, tf_matchmaking_goodenough_hi_count_start.GetFloat(), tf_matchmaking_goodenough_hi_count_end.GetFloat() );
  1572. // !TEST! Hack this to force us to just show some servers
  1573. //fScoreLo = 3.0f;
  1574. //fCountLo = 10.0f;
  1575. // Now scan list to see if we have enough results at this point
  1576. int iGoodEnoughCount = 0;
  1577. for ( iGoodEnoughCount = 0 ; iGoodEnoughCount < vecGameServers.Count() ; ++iGoodEnoughCount)
  1578. {
  1579. sortable_gameserveritem_t *pItem = vecGameServers[ iGoodEnoughCount ];
  1580. if ( pItem->TotalScore() < fScoreLo )
  1581. {
  1582. // This guy and all subsequent ones don't meet the criteria.
  1583. break;
  1584. }
  1585. Assert( pItem->m_eStatus == TF_Gamestats_QuickPlay_t::k_Server_Eligible );
  1586. }
  1587. // See how many more are eligible, just not meeting our criteria to terminate the search
  1588. int iEligibleCount = iGoodEnoughCount;
  1589. while ( iEligibleCount < vecGameServers.Count() && vecGameServers[iEligibleCount]->m_eStatus > TF_Gamestats_QuickPlay_t::k_Server_Ineligible )
  1590. {
  1591. Assert( vecGameServers[iEligibleCount]->m_eStatus == TF_Gamestats_QuickPlay_t::k_Server_Eligible );
  1592. ++iEligibleCount;
  1593. }
  1594. // Update the dialog
  1595. if ( m_pBusyContainer )
  1596. {
  1597. locchar_t wszValue[64];
  1598. locchar_t wszText[256];
  1599. loc_sprintf_safe( wszValue, LOCCHAR( "%u" ), iEligibleCount );
  1600. g_pVGuiLocalize->ConstructString_safe( wszText, g_pVGuiLocalize->Find( "#TF_Quickplay_NumGames" ), 1, wszValue );
  1601. m_pBusyContainer->SetDialogVariable( "numservers", wszText );
  1602. }
  1603. // Found enough good servers to bail now?
  1604. if ( iGoodEnoughCount >= (int)(fCountLo + 0.5f) )
  1605. {
  1606. TF_MATCHMAKING_SPEW(1, "Quickplay initial search: %d servers with scores %.2f or better. Good enough...proceeding to backend scoring.\n", iGoodEnoughCount, fScoreLo );
  1607. }
  1608. else
  1609. {
  1610. // We haven't found enough good servers. Update progress indicator
  1611. // to show that we're doing something
  1612. if ( m_pProgressBar )
  1613. {
  1614. // Determine percentage completion based on "good enough" criteria.
  1615. float fGoodEnoughPct = (float)iGoodEnoughCount / (float)fCountLo;
  1616. // Use max of this indicator, or basic timeout. That way,
  1617. // there is always some motion on the progress bar
  1618. float fProgressPct = MAX( fGoodEnoughPct, fSearchTimePct );
  1619. // Set it
  1620. m_pProgressBar->SetProgress( fProgressPct );
  1621. }
  1622. // Keep searching
  1623. return;
  1624. }
  1625. // OK, we think we've got enough decent servers to see what the GC thinks.
  1626. // No need to keep talking to the GMS
  1627. DestroyServerListRequest();
  1628. }
  1629. // Set progress bar to 100%
  1630. if ( m_pProgressBar )
  1631. {
  1632. m_pProgressBar->SetProgress( 1.0f );
  1633. }
  1634. // Decide how many to score.
  1635. const int iNumServersToScore = MIN( vecGameServers.Count(), kTFMaxQuickPlayServersToScore );
  1636. //
  1637. // Dump some debug stuff to the console
  1638. //
  1639. TF_MATCHMAKING_SPEW( 1, "Quickplay client scoring\n" );
  1640. TF_MATCHMAKING_SPEW( 1, "Selected game mode: %d\n", m_options.m_eSelectedGameType );
  1641. TF_MATCHMAKING_SPEW( 1, "Hours played: %.1f\n", m_fHoursPlayed );
  1642. TF_MATCHMAKING_SPEW( 1, "Valve server bonus: %.2f, noob map bonus: %.2f\n", m_fValveBonus, m_fNoobMapBonus );
  1643. TF_MATCHMAKING_SPEW( 1, "Search duration: %.1f\n", fSearchDuration );
  1644. 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 );
  1645. CUtlSortVector< sortable_gameserveritem_t *, CGameServerItemSort > vecGameServersToPrint;
  1646. if ( tf_matchmaking_debug.GetInt() >= 4 )
  1647. {
  1648. FOR_EACH_MAP_FAST( m_mapServers, idx )
  1649. {
  1650. vecGameServersToPrint.InsertNoSort( &m_mapServers[idx] );
  1651. }
  1652. }
  1653. else if ( tf_matchmaking_debug.GetInt() >= 3 )
  1654. {
  1655. for ( int i = 0 ; i < vecGameServers.Count() ; ++i)
  1656. {
  1657. vecGameServersToPrint.InsertNoSort( vecGameServers[i] );
  1658. }
  1659. }
  1660. else if ( tf_matchmaking_debug.GetInt() >= 2 )
  1661. {
  1662. for ( int i = 0 ; i < iNumServersToScore ; ++i)
  1663. {
  1664. vecGameServersToPrint.InsertNoSort( vecGameServers[i] );
  1665. }
  1666. }
  1667. if ( vecGameServersToPrint.Count() > 0 )
  1668. {
  1669. vecGameServersToPrint.RedoSort();
  1670. Msg( "Name, address, numplayers, maxplayers, ping, new user friendly, registered, valve, recent match penalty, user score, server score, total score, steamID, map, tags\n" );
  1671. // Print in reverse order --- the bottom is the most interesting part and the log usually scrolls.
  1672. // Anybody looking at a file can pull it into excel and sort however they want
  1673. for ( int i = vecGameServersToPrint.Count()-1 ; i >=0 ; --i )
  1674. {
  1675. sortable_gameserveritem_t &item = *vecGameServersToPrint[i];
  1676. Msg( "\"%s\",\"%s\",%d,%d,%d,%d,%d,%d,%.2f,%7.4f,%7.4f,%7.4f,\"%s\",\"%s\",\"%s\"\n",
  1677. item.server.GetName(),
  1678. item.server.m_NetAdr.GetConnectionAddressString(),
  1679. item.server.m_nPlayers - item.server.m_nBotPlayers, item.server.m_nMaxPlayers, item.server.m_nPing,
  1680. item.m_bNewUserFriendly ? 1 : 0,
  1681. item.m_bRegistered ? 1 : 0,
  1682. item.m_bValve ? 1 : 0,
  1683. item.m_fRecentMatchPenalty,
  1684. item.userScore, item.serverScore, item.serverScore+item.userScore,
  1685. item.server.m_steamID.Render(),
  1686. item.server.m_szMap,
  1687. item.server.m_szGameTags
  1688. );
  1689. }
  1690. }
  1691. // *NOW* Check for failure. (We waited until now, so we can properly log it.)
  1692. if ( iNumServersToScore <= 0 )
  1693. {
  1694. C_CTFGameStats::ImmediateWriteInterfaceEvent( "response(quickplay)", "servers_not_found" );
  1695. SendStats( m_mapServers.Count() > 0 ?
  1696. TF_Gamestats_QuickPlay_t::k_Result_NoServersMetCrtieria :
  1697. TF_Gamestats_QuickPlay_t::k_Result_NoServersFound );
  1698. NoServersFoundFailure();
  1699. return;
  1700. }
  1701. // send message to GC
  1702. GCSDK::CProtoBufMsg< CMsgTFQuickplay_ScoreServers > msg( k_EMsgGC_QP_ScoreServers );
  1703. for ( int i = 0; i < iNumServersToScore; ++i )
  1704. {
  1705. sortable_gameserveritem_t *pItem = vecGameServers[i];
  1706. gameserveritem_t &server = pItem->server;
  1707. Assert( pItem->m_eStatus == TF_Gamestats_QuickPlay_t::k_Server_Eligible );
  1708. pItem->m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_RequestedScore;
  1709. CMsgTFQuickplay_ScoreServers_ServerInfo *pServerInfo = msg.Body().add_servers();
  1710. pServerInfo->set_server_address( server.m_NetAdr.GetIP() );
  1711. pServerInfo->set_server_port( server.m_NetAdr.GetConnectionPort() );
  1712. pServerInfo->set_num_users( server.m_nPlayers - server.m_nBotPlayers );
  1713. pServerInfo->set_max_users( server.m_nMaxPlayers );
  1714. pServerInfo->set_user_score( pItem->userScore );
  1715. pServerInfo->set_steam_id( server.m_steamID.ConvertToUint64() );
  1716. }
  1717. m_eCurrentStep = k_EStep_GCScore;
  1718. //// !TEST! Skip scoring
  1719. //CMsgTFQuickplay_ScoreServersResponse emptyMsg;
  1720. //OnReceivedGCScores( emptyMsg );
  1721. //return;
  1722. GCClientSystem()->BSendMessage( msg );
  1723. // Give the GC some time to respond. It usually wil respond very quickly
  1724. m_timeGCScoreTimeout = Plat_FloatTime() + 10.0f;
  1725. }
  1726. void UserConnectToServer()
  1727. {
  1728. Assert( m_eCurrentStep == k_EStep_SelectServerUI );
  1729. if ( m_adrCurrentPing.IsValid() )
  1730. return;
  1731. CQuickListPanel *pPanel = dynamic_cast<CQuickListPanel*>( m_pServerListPanel->GetSelectedPanel() );
  1732. if ( pPanel == NULL )
  1733. return;
  1734. int idx = pPanel->GetListID();
  1735. m_adrCurrentPing = m_mapServers.Key( idx );
  1736. PingServer();
  1737. }
  1738. void QuickpickPingFailed( const char *pszLocTok )
  1739. {
  1740. ShowMessageBox( "#TF_Quickplay_Error", pszLocTok, "#GameUI_OK" );
  1741. // Remove this guy
  1742. for ( int i = 0 ; i < m_vecServerJoinQueue.Count() ; ++i )
  1743. {
  1744. if ( m_vecServerJoinQueue[i].m_adr == m_adrCurrentPing )
  1745. {
  1746. m_vecServerJoinQueue.Remove(i);
  1747. if ( m_pServerListPanel )
  1748. m_pServerListPanel->RemoveItem(i);
  1749. break;
  1750. }
  1751. }
  1752. m_adrCurrentPing.Clear();
  1753. // !KLUDGE! If the list goes empty, then just act like they hit ESC.
  1754. if ( m_pServerListPanel && m_pServerListPanel->GetItemCount() == 0 )
  1755. OnCommand( "user_close" );
  1756. }
  1757. void PingNextBestServer()
  1758. {
  1759. Assert( m_eCurrentStep == k_EStep_PingCheckNotFull );
  1760. // If we already have any ping activity going, cancel it. (We shouldn't)
  1761. DestroyServerQueryRequest();
  1762. // Any more options to try?
  1763. if ( m_vecServerJoinQueue.Count() < 1 )
  1764. {
  1765. TF_MATCHMAKING_SPEW( 1, "No more scored servers left to ping. We failed!\n" );
  1766. SendStats( TF_Gamestats_QuickPlay_t::k_Result_FinalPingFailed );
  1767. NoServersFoundFailure();
  1768. return;
  1769. }
  1770. // Remove the next address from the queue
  1771. m_adrCurrentPing = m_vecServerJoinQueue[0].m_adr;
  1772. m_vecServerJoinQueue.Remove( 0 );
  1773. // Do it
  1774. PingServer();
  1775. }
  1776. void PingServer()
  1777. {
  1778. // If we already have any ping activity going, cancel it. (We shouldn't)
  1779. DestroyServerQueryRequest();
  1780. // Find it in the server list; mark it as being pinged
  1781. int iServerIndex = m_mapServers.Find( m_adrCurrentPing );
  1782. if ( m_mapServers.IsValidIndex(iServerIndex) )
  1783. {
  1784. Assert( m_mapServers[iServerIndex].m_eStatus >= TF_Gamestats_QuickPlay_t::k_Server_Eligible );
  1785. m_mapServers[iServerIndex].m_eStatus = TF_Gamestats_QuickPlay_t::k_Server_Pinged;
  1786. }
  1787. else
  1788. {
  1789. // Wat.
  1790. Assert( m_mapServers.IsValidIndex(iServerIndex) );
  1791. }
  1792. // Send the ping
  1793. TF_MATCHMAKING_SPEW( 2, "Pinging %s\n", m_adrCurrentPing.ToString() );
  1794. m_hServerQueryRequest = steamapicontext->SteamMatchmakingServers()->PingServer( m_adrCurrentPing.GetIPHostByteOrder(), m_adrCurrentPing.GetPort(), this );
  1795. Assert( m_hServerQueryRequest != HSERVERQUERY_INVALID );
  1796. // Set timeout. Since we've already pinged them once
  1797. // fairly recently, and we are considering joining this
  1798. // server, we don't want to wait around forever. They
  1799. // should reply to the ping pretty quickly or let's
  1800. // not join them.
  1801. m_timePingServerTimeout = Plat_FloatTime() + 1.0;
  1802. }
  1803. virtual void OnKeyCodeTyped( vgui::KeyCode code )
  1804. {
  1805. if( code == KEY_ESCAPE )
  1806. {
  1807. OnCommand( "user_close" );
  1808. }
  1809. else
  1810. {
  1811. BaseClass::OnKeyCodeTyped( code );
  1812. }
  1813. }
  1814. };
  1815. class CStandaloneQuickplayMenu : public CQuickplayWaitDialog
  1816. {
  1817. DECLARE_CLASS_SIMPLE( CStandaloneQuickplayMenu, CQuickplayWaitDialog );
  1818. public:
  1819. CStandaloneQuickplayMenu( bool bFeelingLucky, const QuickplaySearchOptions &opt )
  1820. : BaseClass( bFeelingLucky, opt )
  1821. {}
  1822. virtual void GenericFailure() OVERRIDE
  1823. {
  1824. CloseMe();
  1825. ShowMessageBox( "#TF_MM_GenericFailure_Title", "#TF_MM_GenericFailure", "#GameUI_OK", NULL );
  1826. }
  1827. virtual void NoServersFoundFailure() OVERRIDE
  1828. {
  1829. CloseMe();
  1830. ShowMessageBox( "#TF_MM_ResultsDialog_Title", "#TF_MM_ResultsDialog_ServerNotFound", "#GameUI_OK", NULL );
  1831. }
  1832. };
  1833. //-----------------------------------------------------------------------------
  1834. // Purpose: Quickplay Dialog
  1835. //-----------------------------------------------------------------------------
  1836. ConVar tf_quickplay_lastviewedmode( "tf_quickplay_lastviewedmode", "0", FCVAR_CLIENTDLL | FCVAR_ARCHIVE );
  1837. CQuickplayPanelBase::CQuickplayPanelBase( vgui::Panel *parent, const char *name )
  1838. : vgui::EditablePanel( parent, name )
  1839. , m_iCurrentItem( 0 )
  1840. , m_pContainer( NULL )
  1841. , m_bSetInitialSelection( false )
  1842. , m_pPrevPageButton( NULL )
  1843. , m_pNextPageButton( NULL )
  1844. , m_bShowRandomOption( true )
  1845. , m_pSimplifiedOptionsContainer( NULL )
  1846. , m_pAdvOptionsContainer( NULL )
  1847. {
  1848. vgui::HScheme scheme = vgui::scheme()->LoadSchemeFromFileEx( enginevgui->GetPanel( PANEL_CLIENTDLL ), "resource/ClientScheme.res", "ClientScheme" );
  1849. SetScheme(scheme);
  1850. SetProportional( true );
  1851. }
  1852. CQuickplayPanelBase::~CQuickplayPanelBase()
  1853. {
  1854. vgui::ivgui()->RemoveTickSignal( GetVPanel() );
  1855. }
  1856. void CQuickplayPanelBase::ShowItemByGameType( EGameCategory type )
  1857. {
  1858. for ( int i = 0 ; i < m_vecItems.Count() ; ++i )
  1859. {
  1860. if ( m_vecItems[i].gameType == type )
  1861. {
  1862. ShowItemByIndex( i );
  1863. return;
  1864. }
  1865. }
  1866. AssertMsg1( false, "Bogus quickplay type %d", type );
  1867. }
  1868. void CQuickplayPanelBase::ShowItemByIndex( int iItem )
  1869. {
  1870. Assert( iItem >= 0 );
  1871. Assert( iItem < m_vecItems.Size() );
  1872. m_iCurrentItem = iItem;
  1873. QuickplayItem &item = m_vecItems[ m_iCurrentItem ];
  1874. if ( m_pGameModeInfoContainer )
  1875. {
  1876. m_pGameModeInfoContainer->SetDialogVariable( "gametype", g_pVGuiLocalize->Find( item.pTitle ) );
  1877. m_pGameModeInfoContainer->SetDialogVariable( "description", g_pVGuiLocalize->Find( item.pDescription ) );
  1878. m_pGameModeInfoContainer->SetDialogVariable( "complexity", g_pVGuiLocalize->Find( item.pComplexity ) );
  1879. if ( m_pMoreInfoContainer )
  1880. m_pMoreInfoContainer->SetDialogVariable( "more_info", g_pVGuiLocalize->Find( item.pMoreInfo ) );
  1881. vgui::ImagePanel *pImage = dynamic_cast< vgui::ImagePanel* >( m_pGameModeInfoContainer->FindChildByName( "ModeImage" ) );
  1882. if ( pImage )
  1883. {
  1884. pImage->SetImage( GetItemImage( item ) );
  1885. }
  1886. }
  1887. char szTmp[16];
  1888. Q_snprintf(szTmp, 16, "%d/%d", m_iCurrentItem + 1, m_vecItems.Size() );
  1889. if ( m_pSimplifiedOptionsContainer )
  1890. m_pSimplifiedOptionsContainer->SetDialogVariable( "page", szTmp );
  1891. if ( m_pGameModeCombo )
  1892. m_pGameModeCombo->SilentActivateItemByRow( iItem );
  1893. WriteOptionCombosAndSummary();
  1894. }
  1895. const char *CQuickplayPanelBase::GetItemImage( const QuickplayItem& item ) const
  1896. {
  1897. return item.pImage;
  1898. }
  1899. void CQuickplayPanelBase::SetupActionTarget( const char *pPanelName )
  1900. {
  1901. vgui::Panel *pPanel = m_pContainer->FindChildByName( pPanelName, true );
  1902. if ( pPanel )
  1903. {
  1904. pPanel->AddActionSignalTarget( this );
  1905. }
  1906. }
  1907. void CQuickplayPanelBase::AddItem( EGameCategory gameType, const char *pTitle, const char *pDescription, const char *pMoreInfo, const char *pComplexity, const char *pImage, const char *pBetaImage )
  1908. {
  1909. int idx = m_vecAllItems.AddToTail();
  1910. QuickplayItem &item = m_vecAllItems[idx];
  1911. item.gameType = gameType;
  1912. item.pTitle = pTitle;
  1913. item.pDescription = pDescription;
  1914. item.pMoreInfo = pMoreInfo;
  1915. item.pComplexity = pComplexity;
  1916. item.pImage = pImage;
  1917. item.pBetaImage = pBetaImage;
  1918. if ( m_pGameModeCombo )
  1919. m_pGameModeCombo->AddItem( pTitle, NULL );
  1920. }
  1921. void CQuickplayPanelBase::UpdateSelectableItems()
  1922. {
  1923. m_vecItems = m_vecAllItems;
  1924. }
  1925. void CQuickplayPanelBase::ApplySettings( KeyValues *pInResourceData )
  1926. {
  1927. BaseClass::ApplySettings( pInResourceData );
  1928. V_strcpy_safe( m_szEvent247Image, pInResourceData->GetString( "event247_image", "illustrations/gamemode_halloween" ) );
  1929. V_strcpy_safe( m_szCommunityUpdateImage, pInResourceData->GetString( "community_update_image", "illustrations/gamemode_cp" ) );
  1930. }
  1931. void CQuickplayPanelBase::ApplySchemeSettings( vgui::IScheme *pScheme )
  1932. {
  1933. BaseClass::ApplySchemeSettings( pScheme );
  1934. m_pSimplifiedOptionsContainer = dynamic_cast< vgui::EditablePanel *>( FindChildByName( "SimplifiedOptionsContainer", true ) ); Assert( m_pSimplifiedOptionsContainer );
  1935. if ( m_pSimplifiedOptionsContainer )
  1936. {
  1937. m_pGameModeInfoContainer = dynamic_cast< vgui::EditablePanel *>( m_pSimplifiedOptionsContainer->FindChildByName( "ModeInfoContainer", true ) ); Assert( m_pGameModeInfoContainer );
  1938. }
  1939. if ( m_pGameModeInfoContainer )
  1940. {
  1941. m_pMoreInfoContainer = dynamic_cast< vgui::EditablePanel *>( m_pGameModeInfoContainer->FindChildByName( "MoreInfoContainer", true ) ); Assert( m_pMoreInfoContainer );
  1942. }
  1943. m_pPrevPageButton = dynamic_cast< vgui::Button *>( FindChildByName( "PrevPageButton", true ) ); Assert( m_pPrevPageButton );
  1944. m_pNextPageButton = dynamic_cast< vgui::Button *>( FindChildByName( "NextPageButton", true ) ); Assert( m_pNextPageButton );
  1945. m_pMoreOptionsButton = dynamic_cast< vgui::Button *>( FindChildByName( "OptionsButton", true ) );
  1946. m_pAdvOptionsContainer = dynamic_cast< vgui::EditablePanel *>( FindChildByName( "AdvOptionsContainer", true ) );
  1947. m_pGameModeCombo = NULL;
  1948. m_pOptionsSummaryLabel = NULL;
  1949. if ( m_pAdvOptionsContainer )
  1950. {
  1951. Panel *pGameModeOptionContainer = m_pAdvOptionsContainer->FindChildByName( "GameModeOptionContainer", true ); Assert( pGameModeOptionContainer );
  1952. if ( pGameModeOptionContainer )
  1953. {
  1954. m_pGameModeCombo = dynamic_cast< vgui::ComboBox *>( pGameModeOptionContainer->FindChildByName( "OptionCombo", true ) );
  1955. Assert( m_pGameModeCombo );
  1956. }
  1957. if ( m_pGameModeCombo )
  1958. m_pGameModeCombo->AddActionSignalTarget( this );
  1959. if ( m_pContainer )
  1960. {
  1961. m_pOptionsSummaryLabel = dynamic_cast< vgui::Label *>( m_pContainer->FindChildByName( "OptionsSummaryLabel", true ) );
  1962. Assert( m_pOptionsSummaryLabel );
  1963. }
  1964. }
  1965. //m_pFavoritesCheckButton = dynamic_cast< vgui::CheckButton *>( m_pContainer->FindChildByName( "FavoritesCheckButton" ) );
  1966. //m_pFavoritesCheckButton->AddActionSignalTarget( this );
  1967. //
  1968. //m_pRefreshButton = dynamic_cast< vgui::Button *>( m_pContainer->FindChildByName( "RefreshButton" ) );
  1969. //m_pRefreshButton->AddActionSignalTarget( this );
  1970. m_vecItems.RemoveAll();
  1971. m_vecAllItems.RemoveAll();
  1972. // listed in the order we want to show them
  1973. extern bool TF_IsHolidayActive( int eHoliday );
  1974. bool bHalloween = TF_IsHolidayActive( kHoliday_Halloween );
  1975. if ( bHalloween )
  1976. {
  1977. AddItem( kGameCategory_Event247, "#Gametype_Halloween247", "#TF_GameModeDesc_Halloween247", "#TF_GameModeDetail_Halloween247", "#TF_Quickplay_Complexity1", m_szEvent247Image, NULL );
  1978. AddItem( kGameCategory_EventMix, "#Gametype_HalloweenMix", "#TF_GameModeDesc_HalloweenMix", "#TF_GameModeDetail_HalloweenMix", "#TF_Quickplay_Complexity1", "illustrations/gamemode_halloween", NULL );
  1979. }
  1980. // AddItem( kGameType_Community_Update,"#GameType_Community_Update", "#TF_GameModeDesc_Community_Update", "#TF_GameModeDetail_Community_Update", "#TF_Quickplay_Complexity1", m_szCommunityUpdateImage, NULL );
  1981. // AddItem( kGameType_Featured, "#GameType_Featured", "#TF_GameModeDesc_Featured", "#TF_GameModeDetail_Featured", "#TF_Quickplay_Complexity1", "illustrations/gamemode_operation_tough_break", NULL );
  1982. AddItem( kGameCategory_Escort, "#Gametype_Escort", "#TF_GameModeDesc_Escort", "#TF_GameModeDetail_Escort", "#TF_Quickplay_Complexity1", "illustrations/gamemode_payload", "illustrations/gamemode_payload_beta" );
  1983. AddItem( kGameCategory_Koth, "#Gametype_Koth", "#TF_GameModeDesc_Koth", "#TF_GameModeDetail_Koth", "#TF_Quickplay_Complexity1", "illustrations/gamemode_koth", NULL );
  1984. AddItem( kGameCategory_AttackDefense, "#Gametype_AttackDefense", "#TF_GameModeDesc_AttackDefense", "#TF_GameModeDetail_AttackDefense", "#TF_Quickplay_Complexity1", "illustrations/gamemode_attackdefend", NULL );
  1985. AddItem( kGameCategory_EscortRace, "#Gametype_EscortRace", "#TF_GameModeDesc_EscortRace", "#TF_GameModeDetail_EscortRace", "#TF_Quickplay_Complexity2", "illustrations/gamemode_payloadrace", NULL );
  1986. AddItem( kGameCategory_CP, "#Gametype_CP", "#TF_GameModeDesc_CP", "#TF_GameModeDetail_CP", "#TF_Quickplay_Complexity2", "illustrations/gamemode_cp", NULL );
  1987. AddItem( kGameCategory_CTF, "#Gametype_CTF", "#TF_GameModeDesc_CTF", "#TF_GameModeDetail_CTF", "#TF_Quickplay_Complexity2", "illustrations/gamemode_ctf", NULL );
  1988. AddItem( kGameCategory_Misc, "#Gametype_Misc", "#TF_GameModeDesc_Misc", "#TF_GameModeDetail_Misc", "#TF_Quickplay_Complexity2", "illustrations/gamemode_sd", NULL );
  1989. 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
  1990. AddItem( kGameCategory_Passtime, "#GameType_Passtime", "#TF_GameModeDesc_Passtime", "#TF_GameModeDetail_Passtime", "#TF_Quickplay_Complexity2", "illustrations/gamemode_passtime", "illustrations/gamemode_passtime_beta" );
  1991. AddItem( kGameCategory_RobotDestruction,"#Gametype_RobotDestruction", "#TF_GameModeDesc_RobotDestruction", "#TF_GameModeDetail_RobotDestruction", "#TF_Quickplay_Complexity2", "illustrations/gamemode_sd", "illustrations/gamemode_robotdestruction_beta" );
  1992. if ( m_bShowRandomOption )
  1993. AddItem( kGameCategory_Quickplay, "#Gametype_Quickplay", "#TF_GameModeDesc_Quickplay", "#TF_GameModeDetail_Quickplay", "#TF_Quickplay_Complexity2", "illustrations/quickplay", "illustrations/quickplay_beta" );
  1994. // AddItem( kQuickplayGameType_Arena, "#Gametype_Arena", "#TF_GameModeDesc_Arena", "#TF_GameModeDetail_Arena", "#TF_Quickplay_Complexity3", "maps/menu_photos_cp_granary" );
  1995. // AddItem( kQuickplayGameType_Specialty, "#Gametype_Specialty", "#TF_GameModeDesc_Specialty", "#TF_GameModeDetail_Specialty", "#TF_Quickplay_Complexity3", "maps/menu_photos_cp_granary" );
  1996. UpdateSelectableItems();
  1997. SetupMoreOptions();
  1998. // set current, if we didn't already
  1999. if ( !m_bSetInitialSelection )
  2000. {
  2001. m_iCurrentItem = tf_quickplay_lastviewedmode.GetInt();
  2002. static bool bForcedOnce = false;
  2003. //tagES need to remove this when Operation Gun Mettle is finished
  2004. if ( /* bHalloween && */ !bForcedOnce )
  2005. {
  2006. m_iCurrentItem = 0;
  2007. bForcedOnce = true;
  2008. }
  2009. m_bSetInitialSelection = true;
  2010. }
  2011. m_iCurrentItem = Min( m_iCurrentItem, m_vecItems.Count()-1 );
  2012. m_iCurrentItem = Max( m_iCurrentItem, 0 );
  2013. ShowItemByIndex( m_iCurrentItem );
  2014. SetupActionTarget( "MoreInfoButton" );
  2015. SetupActionTarget( "PrevPageButton" );
  2016. SetupActionTarget( "NextPageButton" );
  2017. SetupActionTarget( "CancelButton" );
  2018. ShowSimplifiedOrAdvancedOptions();
  2019. }
  2020. void CQuickplayPanelBase::SetupMoreOptions()
  2021. {
  2022. SetupActionTarget( "OptionsButton" );
  2023. // Panel *pOptionTemplate = m_pMoreOptionsContainer->FindChildByName( "OptionContainerTemplate" );
  2024. // Assert( pOptionTemplate );
  2025. // KeyValues *pTemplateSettings = new KeyValues( "Template" );
  2026. // pOptionTemplate->GetSettings( pTemplateSettings );
  2027. // pOptionTemplate->SetVisible( false );
  2028. //
  2029. // new QuickplayOptionPanel( m_pMoreOptionsContainer, 0, pTemplateSettings );
  2030. // new QuickplayOptionPanel( m_pMoreOptionsContainer, 1, pTemplateSettings );
  2031. // new QuickplayOptionPanel( m_pMoreOptionsContainer, 2, pTemplateSettings );
  2032. //
  2033. // pTemplateSettings->deleteThis();
  2034. AdvOption *pOpt;
  2035. COMPILE_TIME_ASSERT( kEAdvOption_ServerHost == 0 );
  2036. pOpt = &m_vecAdvOptions[ m_vecAdvOptions.AddToTail() ];
  2037. pOpt->m_pszContainerName = "ValveServerOption";
  2038. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_ServerHost_Official" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_ServerHost_Official_Summary" );
  2039. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_ServerHost_Community" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_ServerHost_Community_Summary" );
  2040. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_ServerHost_DontCare" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_ServerHost_DontCare_Summary" );
  2041. pOpt->m_pConvar = &tf_quickplay_pref_community_servers;
  2042. COMPILE_TIME_ASSERT( kEAdvOption_MaxPlayers == 1 );
  2043. pOpt = &m_vecAdvOptions[ m_vecAdvOptions.AddToTail() ];
  2044. pOpt->m_pszContainerName = "IncreasedPlayerCountOption";
  2045. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_MaxPlayers_Default" ); pOpt->m_vecOptionSummaryNames.AddToTail( "" );
  2046. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_MaxPlayers_Increased" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_MaxPlayers_Increased_Summary" );
  2047. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_MaxPlayers_DontCare" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_MaxPlayers_DontCare_Summary" );
  2048. pOpt->m_pConvar = &tf_quickplay_pref_increased_maxplayers;
  2049. COMPILE_TIME_ASSERT( kEAdvOption_Respawn == 2 );
  2050. pOpt = &m_vecAdvOptions[ m_vecAdvOptions.AddToTail() ];
  2051. pOpt->m_pszContainerName = "RespawnTimesOption";
  2052. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_RespawnTimes_Default" ); pOpt->m_vecOptionSummaryNames.AddToTail( "" );
  2053. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_RespawnTimes_Instant" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_RespawnTimes_Instant_Summary" );
  2054. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_RespawnTimes_DontCare" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_RespawnTimes_DontCare_Summary" );
  2055. pOpt->m_pConvar = &tf_quickplay_pref_respawn_times;
  2056. COMPILE_TIME_ASSERT( kEAdvOption_RandomCrits == 3 );
  2057. pOpt = &m_vecAdvOptions[ m_vecAdvOptions.AddToTail() ];
  2058. pOpt->m_pszContainerName = "RandomCritsOption";
  2059. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_RandomCrits_Default" ); pOpt->m_vecOptionSummaryNames.AddToTail( "" );
  2060. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_RandomCrits_Disabled" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_RandomCrits_Disabled_Summary" );
  2061. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_RandomCrits_DontCare" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_RandomCrits_DontCare_Summary" );
  2062. pOpt->m_pConvar = &tf_quickplay_pref_disable_random_crits;
  2063. COMPILE_TIME_ASSERT( kEAdvOption_DamageSpread == 4 );
  2064. pOpt = &m_vecAdvOptions[ m_vecAdvOptions.AddToTail() ];
  2065. pOpt->m_pszContainerName = "DamageSpreadOption";
  2066. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_DamageSpread_Default" ); pOpt->m_vecOptionSummaryNames.AddToTail( "" );
  2067. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_DamageSpread_Enabled" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_DamageSpread_Enabled_Summary" );
  2068. pOpt->m_vecOptionNames.AddToTail( "#TF_Quickplay_DamageSpread_DontCare" ); pOpt->m_vecOptionSummaryNames.AddToTail( "#TF_Quickplay_DamageSpread_DontCare_Summary" );
  2069. pOpt->m_pConvar = &tf_quickplay_pref_enable_damage_spread;
  2070. FOR_EACH_VEC( m_vecAdvOptions, idxAdvOpt )
  2071. {
  2072. pOpt = &m_vecAdvOptions[ idxAdvOpt ];
  2073. EditablePanel *pContainer = dynamic_cast<EditablePanel*>( FindChildByName( pOpt->m_pszContainerName, true ) );
  2074. FOR_EACH_VEC( pOpt->m_vecOptionNames, idx )
  2075. {
  2076. vgui::RadioButton *pRadioButton = NULL;
  2077. if ( pContainer )
  2078. {
  2079. pRadioButton = dynamic_cast<vgui::RadioButton*>( pContainer->FindChildByName( VarArgs("RadioButton%d", idx ) ) );
  2080. Assert( pRadioButton );
  2081. }
  2082. if ( pRadioButton )
  2083. {
  2084. pRadioButton->AddActionSignalTarget( this );
  2085. pRadioButton->SetText( pOpt->m_vecOptionNames[idx] );
  2086. }
  2087. pOpt->m_vecRadioButtons.AddToTail( pRadioButton );
  2088. }
  2089. }
  2090. // Populate choice from the convars
  2091. FOR_EACH_VEC( m_vecAdvOptions, idxAdvOpt )
  2092. {
  2093. AdvOption *pOpt = &m_vecAdvOptions[ idxAdvOpt ];
  2094. pOpt->m_nChoice = pOpt->m_pConvar->GetInt();
  2095. }
  2096. }
  2097. void CQuickplayPanelBase::ShowSimplifiedOrAdvancedOptions()
  2098. {
  2099. if ( m_pSimplifiedOptionsContainer )
  2100. m_pSimplifiedOptionsContainer->SetVisible( !tf_quickplay_pref_advanced_view.GetBool() );
  2101. if ( m_pAdvOptionsContainer )
  2102. m_pAdvOptionsContainer->SetVisible( tf_quickplay_pref_advanced_view.GetBool() );
  2103. }
  2104. const size_t kQuickplayOptionsSummaryLen = 1024;
  2105. static void AppendOptionInfo( wchar_t *wszText, const char *pszLocToken )
  2106. {
  2107. if ( pszLocToken == NULL || *pszLocToken == '\0' )
  2108. return;
  2109. const wchar_t *pwszLocalized = NULL;
  2110. if ( *pszLocToken == '#' )
  2111. pwszLocalized = g_pVGuiLocalize->Find( pszLocToken );
  2112. wchar_t wszTemp[ 1024 ];
  2113. if ( pwszLocalized == NULL )
  2114. {
  2115. V_UTF8ToUnicode( pszLocToken, wszTemp, sizeof(wszTemp) );
  2116. pwszLocalized = wszTemp;
  2117. }
  2118. if ( *wszText != '\0' )
  2119. V_wcsncat( wszText, L"; ", kQuickplayOptionsSummaryLen );
  2120. V_wcsncat( wszText, pwszLocalized, kQuickplayOptionsSummaryLen );
  2121. }
  2122. void CQuickplayPanelBase::WriteOptionCombosAndSummary()
  2123. {
  2124. wchar_t wszSmmary[ kQuickplayOptionsSummaryLen ] = {};
  2125. GetOptionsAndSummaryText( wszSmmary );
  2126. if ( m_pOptionsSummaryLabel )
  2127. m_pOptionsSummaryLabel->SetText( wszSmmary );
  2128. }
  2129. void CQuickplayPanelBase::GetOptionsAndSummaryText( wchar_t *pwszSummary )
  2130. {
  2131. if ( m_vecItems[m_iCurrentItem].gameType == kGameCategory_Quickplay )
  2132. AppendOptionInfo( pwszSummary, "#Gametype_AnyGameMode" ); // "random" looks weird here, use custom text
  2133. else
  2134. AppendOptionInfo( pwszSummary, m_vecItems[m_iCurrentItem].pTitle );
  2135. // Check if we need to force "community only" or "don't care" server host due to their options
  2136. FOR_EACH_VEC( m_vecAdvOptions, idxVecOpt )
  2137. {
  2138. if ( idxVecOpt == kEAdvOption_ServerHost )
  2139. continue;
  2140. if ( m_vecAdvOptions[idxVecOpt].m_nChoice == 1 )
  2141. {
  2142. m_vecAdvOptions[kEAdvOption_ServerHost].m_nChoice = QuickplaySearchOptions::eServersCommunity;
  2143. break;
  2144. }
  2145. }
  2146. FOR_EACH_VEC( m_vecAdvOptions, idxAdvOpt )
  2147. {
  2148. AdvOption *pOpt = &m_vecAdvOptions[ idxAdvOpt ];
  2149. FOR_EACH_VEC( pOpt->m_vecRadioButtons, idxRadButton )
  2150. {
  2151. if ( pOpt->m_vecRadioButtons[idxRadButton] )
  2152. {
  2153. //pOpt->m_vecRadioButtons[idxRadButton]->SetEnabled( idxRadButton == 0 || !bForceVanilla );
  2154. pOpt->m_vecRadioButtons[idxRadButton]->SilentSetSelected( pOpt->m_nChoice == idxRadButton );
  2155. }
  2156. }
  2157. int nChoice = ( tf_quickplay_pref_beta_content.GetBool() ? 0 : pOpt->m_nChoice );
  2158. AppendOptionInfo( pwszSummary, pOpt->m_vecOptionSummaryNames[nChoice] );
  2159. }
  2160. }
  2161. void CQuickplayPanelBase::OnTextChanged( vgui::Panel *panel )
  2162. {
  2163. if ( panel == m_pGameModeCombo )
  2164. {
  2165. UserSelectItemByIndex( m_pGameModeCombo->GetActiveItem() );
  2166. return;
  2167. }
  2168. }
  2169. void CQuickplayPanelBase::OnRadioButtonChecked( vgui::Panel *panel )
  2170. {
  2171. FOR_EACH_VEC( m_vecAdvOptions, idxAdvOpt )
  2172. {
  2173. AdvOption *pOpt = &m_vecAdvOptions[ idxAdvOpt ];
  2174. int idxBtn = pOpt->m_vecRadioButtons.Find( (vgui::RadioButton*)panel );
  2175. if ( idxBtn >= 0 )
  2176. {
  2177. pOpt->m_nChoice = idxBtn;
  2178. // Check if they change the server hosting to valve servers,
  2179. // then slam their choices to vanilla
  2180. if ( idxAdvOpt == kEAdvOption_ServerHost && idxBtn == QuickplaySearchOptions::eServersOfficial )
  2181. {
  2182. FOR_EACH_VEC( m_vecAdvOptions, idxSlam )
  2183. {
  2184. if ( idxSlam != kEAdvOption_ServerHost )
  2185. m_vecAdvOptions[idxSlam].m_nChoice = 0;
  2186. }
  2187. }
  2188. }
  2189. }
  2190. WriteOptionCombosAndSummary();
  2191. }
  2192. void CQuickplayPanelBase::SetPageScrollButtonsVisible( bool bFlag )
  2193. {
  2194. if ( m_pPrevPageButton )
  2195. m_pPrevPageButton->SetVisible( bFlag );
  2196. if ( m_pNextPageButton )
  2197. m_pNextPageButton->SetVisible( bFlag );
  2198. }
  2199. void CQuickplayPanelBase::SaveSettings()
  2200. {
  2201. tf_quickplay_lastviewedmode.SetValue( m_iCurrentItem );
  2202. FOR_EACH_VEC( m_vecAdvOptions, idxAdvOpt )
  2203. {
  2204. AdvOption *pOpt = &m_vecAdvOptions[ idxAdvOpt ];
  2205. pOpt->m_pConvar->SetValue( pOpt->m_nChoice );
  2206. }
  2207. }
  2208. void CQuickplayPanelBase::OnCommand( const char *pCommand )
  2209. {
  2210. if ( FStrEq( pCommand, "prevpage" ) )
  2211. {
  2212. UserSelectItemByIndex( ( m_iCurrentItem > 0 ) ? ( m_iCurrentItem - 1 ) : ( m_vecItems.Count() - 1 ) );
  2213. if ( m_pPrevPageButton )
  2214. m_pPrevPageButton->RequestFocus();
  2215. }
  2216. else if ( FStrEq( pCommand, "nextpage" ) )
  2217. {
  2218. UserSelectItemByIndex( ( m_iCurrentItem+1 < m_vecItems.Count() ) ? ( m_iCurrentItem+1 ) : 0 );
  2219. if ( m_pNextPageButton )
  2220. m_pNextPageButton->RequestFocus();
  2221. }
  2222. else if ( FStrEq( pCommand, "more_info" ) )
  2223. {
  2224. if ( m_pMoreInfoContainer && m_pGameModeInfoContainer )
  2225. {
  2226. m_pMoreInfoContainer->SetVisible( !m_pMoreInfoContainer->IsVisible() );
  2227. vgui::ImagePanel *pImage = dynamic_cast< vgui::ImagePanel* >( m_pGameModeInfoContainer->FindChildByName( "ModeImage" ) );
  2228. if ( pImage )
  2229. {
  2230. pImage->SetVisible( !m_pMoreInfoContainer->IsVisible() );
  2231. }
  2232. }
  2233. }
  2234. else if ( FStrEq( pCommand, "ToggleShowOptions" ) )
  2235. {
  2236. tf_quickplay_pref_advanced_view.SetValue( !tf_quickplay_pref_advanced_view.GetBool() );
  2237. ShowSimplifiedOrAdvancedOptions();
  2238. }
  2239. else
  2240. {
  2241. BaseClass::OnCommand( pCommand );
  2242. }
  2243. }
  2244. void CQuickplayPanelBase::UserSelectItemByIndex( int iNewItem )
  2245. {
  2246. ShowItemByIndex( iNewItem );
  2247. }
  2248. class CQuickplayDialog : public CQuickplayPanelBase
  2249. {
  2250. DECLARE_CLASS_SIMPLE( CQuickplayDialog, CQuickplayPanelBase );
  2251. public:
  2252. CQuickplayDialog( vgui::Panel *parent )
  2253. : CQuickplayPanelBase( parent, "QuickPlayDialog" )
  2254. {
  2255. m_pContainer = new vgui::EditablePanel( this, "Container" );
  2256. m_pBetaCheckButton = new vgui::CheckButton( m_pContainer, "BetaCheckButton", "BetaToggle" );
  2257. m_pBetaCheckButton->AddActionSignalTarget( this );
  2258. m_pBetaCheckButton->SetSelected( tf_quickplay_pref_beta_content.GetInt() == 1 );
  2259. m_pTauntsExplanationPopup = new CExplanationPopup( m_pContainer, "BetaExplanation" );
  2260. C_CTFGameStats::ImmediateWriteInterfaceEvent( "interface_open", "quickplay" );
  2261. LoadControlSettings( "Resource/ui/QuickplayDialog.res" );
  2262. }
  2263. virtual ~CQuickplayDialog()
  2264. {
  2265. C_CTFGameStats::ImmediateWriteInterfaceEvent( "interface_close", "quickplay" );
  2266. }
  2267. virtual void ApplySchemeSettings( vgui::IScheme *pScheme ) OVERRIDE
  2268. {
  2269. BaseClass::ApplySchemeSettings( pScheme );
  2270. Panel *pPanel;
  2271. pPanel = FindChildByName( "PlayNowButton", true ); Assert( pPanel );
  2272. if ( pPanel ) pPanel->AddActionSignalTarget( this );
  2273. pPanel = FindChildByName( "ShowServersButton", true ); Assert( pPanel );
  2274. if ( pPanel ) pPanel->AddActionSignalTarget( this );
  2275. pPanel = FindChildByName( "ExplainBetaButton", true ); Assert( pPanel );
  2276. if ( pPanel ) pPanel->AddActionSignalTarget( this );
  2277. }
  2278. virtual void PerformLayout() OVERRIDE
  2279. {
  2280. m_pBetaCheckButton->SizeToContents();
  2281. BaseClass::PerformLayout();
  2282. // Center it, keeping requested size
  2283. int x, y, ww, wt, wide, tall;
  2284. vgui::surface()->GetWorkspaceBounds( x, y, ww, wt );
  2285. GetSize(wide, tall);
  2286. SetPos(x + ((ww - wide) / 2), y + ((wt - tall) / 2));
  2287. m_pMoreOptionsButton->SetVisible( !m_pBetaCheckButton->IsSelected() );
  2288. tf_quickplay_pref_beta_content.SetValue( m_pBetaCheckButton->IsSelected() ? 1 : 0 );
  2289. // @todo setup
  2290. }
  2291. virtual void Show()
  2292. {
  2293. TFModalStack()->PushModal( this );
  2294. // Make sure we're signed on
  2295. if ( !CheckSteamSignOn() )
  2296. {
  2297. Close();
  2298. return;
  2299. }
  2300. SetVisible( true );
  2301. MakePopup();
  2302. MoveToFront();
  2303. SetKeyBoardInputEnabled( true );
  2304. SetMouseInputEnabled( true );
  2305. vgui::Panel *pPlayNowButton = FindChildByName( "PlayNowButton" );
  2306. if ( pPlayNowButton )
  2307. {
  2308. pPlayNowButton->RequestFocus();
  2309. }
  2310. }
  2311. bool CheckSteamSignOn()
  2312. {
  2313. // Make sure we are connected to steam, or they are going to be disappointed
  2314. if ( steamapicontext == NULL
  2315. || steamapicontext->SteamUtils() == NULL
  2316. || steamapicontext->SteamMatchmakingServers() == NULL
  2317. || steamapicontext->SteamUser() == NULL
  2318. || !steamapicontext->SteamUser()->BLoggedOn()
  2319. ) {
  2320. Warning( "Steam not properly initialized or connected.\n" );
  2321. ShowMessageBox( "#TF_MM_GenericFailure_Title", "#TF_MM_GenericFailure", "#GameUI_OK" );
  2322. return false;
  2323. }
  2324. return true;
  2325. }
  2326. virtual void OnCommand( const char *pCommand )
  2327. {
  2328. C_CTFGameStats::ImmediateWriteInterfaceEvent( "on_command(quickplay)", pCommand );
  2329. if ( FStrEq( pCommand, "playnow" ) || FStrEq( pCommand, "show_servers" ) )
  2330. {
  2331. SaveSettings();
  2332. Close();
  2333. if ( !CheckSteamSignOn() )
  2334. {
  2335. return;
  2336. }
  2337. bool bBetaContent = tf_quickplay_pref_beta_content.GetBool();
  2338. QuickplaySearchOptions opt;
  2339. opt.m_eSelectedGameType = m_vecItems[m_iCurrentItem].gameType;
  2340. opt.m_eServers = (QuickplaySearchOptions::EServers)( bBetaContent ?
  2341. #ifdef STAGING_ONLY
  2342. 2 :
  2343. #else
  2344. 0 :
  2345. #endif
  2346. tf_quickplay_pref_community_servers.GetInt() );
  2347. opt.m_eRandomCrits = (QuickplaySearchOptions::ERandomCrits)( bBetaContent ? 2 : tf_quickplay_pref_disable_random_crits.GetInt() );
  2348. opt.m_eDamageSpread = (QuickplaySearchOptions::EDamageSpread)( bBetaContent ? 2 : tf_quickplay_pref_enable_damage_spread.GetInt() );
  2349. opt.m_eRespawnTimes = (QuickplaySearchOptions::ERespawnTimes)( bBetaContent ? 2 : tf_quickplay_pref_respawn_times.GetInt() );
  2350. opt.m_eMaxPlayers = (QuickplaySearchOptions::EMaxPlayers)( bBetaContent ? 2 : tf_quickplay_pref_increased_maxplayers.GetInt() );
  2351. opt.m_eBetaContent = (QuickplaySearchOptions::EBetaContent)tf_quickplay_pref_beta_content.GetInt();
  2352. ShowWaitingDialog( new CQuickplayWaitDialog( FStrEq( pCommand, "playnow" ), opt ), NULL, true, true, 0.0f );
  2353. }
  2354. else if ( FStrEq( pCommand, "cancel" ) )
  2355. {
  2356. Close();
  2357. SaveSettings();
  2358. }
  2359. else if ( FStrEq( pCommand, "beta_toggle" ) )
  2360. {
  2361. UpdateSelectableItems();
  2362. if ( m_pMoreOptionsButton )
  2363. {
  2364. // Disable the advanced filtering if beta box is checked
  2365. m_pMoreOptionsButton->SetVisible( !m_pBetaCheckButton->IsSelected() );
  2366. tf_quickplay_pref_beta_content.SetValue( m_pBetaCheckButton->IsSelected() ? 1 : 0 );
  2367. WriteOptionCombosAndSummary();
  2368. tf_quickplay_pref_advanced_view.SetValue( 0 );
  2369. ShowSimplifiedOrAdvancedOptions();
  2370. }
  2371. }
  2372. else if ( FStrEq( pCommand, "explain_beta" ) )
  2373. {
  2374. m_pTauntsExplanationPopup->Popup();
  2375. }
  2376. else
  2377. {
  2378. BaseClass::OnCommand( pCommand );
  2379. }
  2380. }
  2381. virtual void OnKeyCodePressed( vgui::KeyCode code )
  2382. {
  2383. vgui::KeyCode nButtonCode = GetBaseButtonCode( code );
  2384. if( nButtonCode == KEY_XBUTTON_B || nButtonCode == STEAMCONTROLLER_B || nButtonCode == STEAMCONTROLLER_START )
  2385. {
  2386. OnCommand( "cancel" );
  2387. }
  2388. else if ( nButtonCode == KEY_XBUTTON_A || nButtonCode == STEAMCONTROLLER_A )
  2389. {
  2390. OnCommand( "playnow" );
  2391. }
  2392. else if ( nButtonCode == KEY_XBUTTON_X || nButtonCode == STEAMCONTROLLER_X )
  2393. {
  2394. OnCommand( "more_info" );
  2395. }
  2396. else if ( nButtonCode == KEY_XBUTTON_LEFT ||
  2397. nButtonCode == KEY_XSTICK1_LEFT ||
  2398. nButtonCode == KEY_XSTICK2_LEFT ||
  2399. nButtonCode == STEAMCONTROLLER_DPAD_LEFT ||
  2400. code == KEY_LEFT )
  2401. {
  2402. OnCommand( "prevpage" );
  2403. }
  2404. else if ( nButtonCode == KEY_XBUTTON_RIGHT ||
  2405. nButtonCode == KEY_XSTICK1_RIGHT ||
  2406. nButtonCode == KEY_XSTICK2_RIGHT ||
  2407. nButtonCode == STEAMCONTROLLER_DPAD_RIGHT ||
  2408. code == KEY_RIGHT )
  2409. {
  2410. OnCommand( "nextpage" );
  2411. }
  2412. else
  2413. {
  2414. BaseClass::OnKeyCodeTyped( code );
  2415. }
  2416. }
  2417. virtual void OnKeyCodeTyped( vgui::KeyCode code )
  2418. {
  2419. if( code == KEY_ESCAPE )
  2420. {
  2421. OnCommand( "cancel" );
  2422. }
  2423. else if ( code == KEY_ENTER || code == KEY_SPACE )
  2424. {
  2425. OnCommand( "playnow" );
  2426. }
  2427. else
  2428. {
  2429. BaseClass::OnKeyCodeTyped( code );
  2430. }
  2431. }
  2432. protected:
  2433. virtual const char *GetItemImage( const QuickplayItem& item ) const OVERRIDE
  2434. {
  2435. if ( m_pBetaCheckButton->IsSelected() && item.pBetaImage )
  2436. {
  2437. return item.pBetaImage;
  2438. }
  2439. return item.pImage;
  2440. }
  2441. virtual void GetOptionsAndSummaryText( wchar_t *pwszSummary ) OVERRIDE
  2442. {
  2443. BaseClass::GetOptionsAndSummaryText( pwszSummary );
  2444. if ( m_pBetaCheckButton->IsSelected() )
  2445. {
  2446. AppendOptionInfo( pwszSummary, "#TF_Quickplay_Beta" );
  2447. }
  2448. }
  2449. // called when the Cancel button is pressed
  2450. void Close()
  2451. {
  2452. SetVisible( false );
  2453. TFModalStack()->PopModal( this );
  2454. MarkForDeletion();
  2455. }
  2456. void UpdateSelectableItems() OVERRIDE
  2457. {
  2458. m_vecItems.Purge();
  2459. bool bBetaActive = m_pBetaCheckButton->IsSelected();
  2460. // Go through each of the modes
  2461. FOR_EACH_VEC( m_vecAllItems, i )
  2462. {
  2463. int nNumWithBetaContent = 0;
  2464. int nNumForThisMode = 0;
  2465. // Go through each of the modes
  2466. for ( int j = 0 ; j < GetItemSchema()->GetMapCount(); ++j )
  2467. {
  2468. const SchemaMap_t& map = GetItemSchema()->GetMapForIndex( j );
  2469. // Tally up maps for this mode
  2470. if ( map.eGameCategory == m_vecAllItems[i].gameType )
  2471. {
  2472. nNumForThisMode++;
  2473. // Check if any of the tags has "beta" as a tag, and tally that if so
  2474. for( int k = 0; k < map.vecTags.Count(); ++k )
  2475. {
  2476. if ( map.vecTags.HasElement( GetItemSchema()->GetHandleForTag( "beta" ) ) )
  2477. {
  2478. nNumWithBetaContent++;
  2479. }
  2480. }
  2481. }
  2482. }
  2483. // Only add the visible items if we're filtering for beta and this category has at least 1 beta map
  2484. // OR if we're not filtering for beta and we have at least 1 map that's not beta
  2485. const bool bBetaFilteringAndHasBetaContent = ( bBetaActive && nNumWithBetaContent > 0 );
  2486. const bool bNotBetaFilteringHasNonBetaContent = ( !bBetaActive && nNumForThisMode > nNumWithBetaContent );
  2487. const bool bIsRandom = m_vecAllItems[i].gameType == kGameCategory_Quickplay;
  2488. if ( ( bBetaFilteringAndHasBetaContent ) || ( bNotBetaFilteringHasNonBetaContent ) || bIsRandom )
  2489. {
  2490. m_vecItems.AddToTail( m_vecAllItems[i] );
  2491. }
  2492. }
  2493. // Go back to the first page
  2494. ShowItemByIndex( 0 );
  2495. }
  2496. private:
  2497. vgui::CheckButton *m_pBetaCheckButton;
  2498. CExplanationPopup *m_pTauntsExplanationPopup;
  2499. };
  2500. static vgui::DHANDLE<CQuickplayDialog> g_pQuickplayDialog;
  2501. //-----------------------------------------------------------------------------
  2502. class CGCTFQuickplay_ScoreServers_Response : public GCSDK::CGCClientJob
  2503. {
  2504. public:
  2505. CGCTFQuickplay_ScoreServers_Response( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {}
  2506. virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
  2507. {
  2508. GCSDK::CProtoBufMsg<CMsgTFQuickplay_ScoreServersResponse> msg( pNetPacket );
  2509. if ( s_pQuickPlayWaitingDialog )
  2510. {
  2511. // !TEST! Forced failure
  2512. //Warning( "CMsgTFQuickplay_ScoreServersResponse received, but discarded to simulate failure!!\n" );
  2513. //return true;
  2514. s_pQuickPlayWaitingDialog->OnReceivedGCScores( msg.Body() );
  2515. }
  2516. else
  2517. {
  2518. Warning(" Received CGCTFQuickplay_ScoreServers_Response, but no quick play query in progress dialog to receive them?\n" );
  2519. }
  2520. return true;
  2521. }
  2522. };
  2523. GC_REG_JOB( GCSDK::CGCClient, CGCTFQuickplay_ScoreServers_Response, "CGCTFQuickplay_ScoreServers_Response", k_EMsgGC_QP_ScoreServersResponse, GCSDK::k_EServerTypeGCClient );
  2524. //-----------------------------------------------------------------------------
  2525. // Purpose: Callback to open the game menus
  2526. //-----------------------------------------------------------------------------
  2527. #ifdef ENABLE_GC_MATCHMAKING
  2528. 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" );
  2529. 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." );
  2530. void QuickplayBetaConfirmCallback( bool bConfirmed, void *pContext )
  2531. {
  2532. tf_quickplay_beta_preference.SetValue( bConfirmed ? 1 : 0 );
  2533. if ( bConfirmed )
  2534. {
  2535. engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby quickplay" );
  2536. }
  2537. else
  2538. {
  2539. engine->ClientCmd_Unrestricted( "OpenQuickplayDialog nobeta" );
  2540. }
  2541. }
  2542. #endif // #ifdef ENABLE_GC_MATCHMAKING
  2543. static void CL_OpenQuickplayDialog( const CCommand &args )
  2544. {
  2545. // Check for opting into beta quickplay
  2546. #ifdef ENABLE_GC_MATCHMAKING
  2547. if ( args.ArgC() < 2 && tf_quickplay_beta_preference.GetInt() != 0 )
  2548. {
  2549. if ( tf_quickplay_beta_preference.GetInt() > 0 )
  2550. {
  2551. engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby quickplay" );
  2552. return;
  2553. }
  2554. if ( steamapicontext && steamapicontext->SteamUser() && ( steamapicontext->SteamUser()->GetSteamID().GetAccountID() % 100U ) < (uint32)tf_quickplay_beta_ask_percentage.GetInt() )
  2555. {
  2556. ShowConfirmDialog( "#TF_QuickplayBeta_OptIn_Title", "#TF_QuickplayBeta_OptIn_Message", "#TF_QuickplayBeta_OptIn_YesButton", "#TF_QuickplayBeta_OptIn_NoButton", QuickplayBetaConfirmCallback );
  2557. return;
  2558. }
  2559. }
  2560. #endif
  2561. if ( g_pQuickplayDialog.Get() == NULL )
  2562. {
  2563. IViewPortPanel *pMMOverride = ( gViewPortInterface->FindPanelByName( PANEL_MAINMENUOVERRIDE ) );
  2564. g_pQuickplayDialog = vgui::SETUP_PANEL( new CQuickplayDialog( (CHudMainMenuOverride*)pMMOverride ) );
  2565. }
  2566. g_pQuickplayDialog->Show();
  2567. }
  2568. // the console commands
  2569. static ConCommand quickplaydialog( "OpenQuickplayDialog", &CL_OpenQuickplayDialog, "Displays the quickplay dialog." );
  2570. static void CL_OpenQuickplayDialogForMap( const CCommand &args )
  2571. {
  2572. if ( args.ArgC() != 2 )
  2573. return;
  2574. QuickplaySearchOptions opt;
  2575. opt.m_eServers = QuickplaySearchOptions::eServersOfficial; // Quests only work on official.
  2576. // Default settings
  2577. opt.m_eRandomCrits = QuickplaySearchOptions::ERandomCrits::eRandomCritsYes;
  2578. opt.m_eDamageSpread = QuickplaySearchOptions::EDamageSpread::eDamageSpreadNo;
  2579. opt.m_eRespawnTimes = QuickplaySearchOptions::ERespawnTimes::eRespawnTimesDefault;
  2580. opt.m_eMaxPlayers = QuickplaySearchOptions::EMaxPlayers::eMaxPlayers24;
  2581. opt.m_eBetaContent = QuickplaySearchOptions::EBetaContent::eBetaNo;
  2582. opt.m_strMapName = args.Arg(1); // Use the map name passed in
  2583. opt.m_eSelectedGameType = kGameCategory_Quickplay;
  2584. ShowWaitingDialog( new CStandaloneQuickplayMenu( false, opt ), NULL, true, true, 0.0f );
  2585. }
  2586. static ConCommand OpenQuickplayWaitDialogForMap( "OpenQuickplayWaitDialogForMap", &CL_OpenQuickplayDialogForMap, "Instantly starts a quickplay query for a map." );