summaryrefslogtreecommitdiff
path: root/game/client/tf/tf_gc_client.cpp
diff options
context:
space:
mode:
authorFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
committerFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
commit3bf9df6b2785fa6d951086978a3e66f49427166a (patch)
tree2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/client/tf/tf_gc_client.cpp
downloadarchived-source-engine-2018-hl2-src-master.tar.xz
archived-source-engine-2018-hl2-src-master.zip
Diffstat (limited to 'game/client/tf/tf_gc_client.cpp')
-rw-r--r--game/client/tf/tf_gc_client.cpp4706
1 files changed, 4706 insertions, 0 deletions
diff --git a/game/client/tf/tf_gc_client.cpp b/game/client/tf/tf_gc_client.cpp
new file mode 100644
index 0000000..f67f10c
--- /dev/null
+++ b/game/client/tf/tf_gc_client.cpp
@@ -0,0 +1,4706 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+#include "cbase.h"
+#include "tf_gc_client.h"
+#include "gcsdk/gcsdk_auto.h"
+#include "tf_gcmessages.h"
+#include "kvpacker.h"
+#include "tf_party.h"
+// XXX(JohnS): Eventually, we want to send a smaller lobby object to clients. For now, they use the CTFGSLobby, which is
+// in shared code for that reason.
+#include "tf_lobby_server.h"
+#include "base_gcmessages.pb.h"
+#include "igameevents.h"
+#include "netadr.h"
+#include "protocol.h"
+#include "econ_item_inventory.h"
+#include "tf_item_inventory.h"
+#include "tf_hud_mann_vs_machine_status.h"
+#include "econ/confirm_dialog.h"
+#include "rtime.h"
+#include "ienginevgui.h"
+#include "clientmode_tf.h"
+#include "tf_match_description.h"
+#include "tf_xp_source.h"
+#include "tf_notification.h"
+#include "c_tf_notification.h"
+#include "tf_gc_shared.h"
+#include <google/protobuf/text_format.h>
+#include "tf_match_join_handlers.h"
+#include "tf_matchmaking_dashboard.h"
+#include "tf_ladder_data.h"
+#include "tf_rating_data.h"
+
+#include "econ_item_description.h"
+
+#include "tf_hud_disconnect_prompt.h"
+
+#include "util_shared.h"
+#include <steamnetworkingsockets/isteamnetworkingutils.h>
+#include <steamnetworkingsockets/isteamnetworkingsockets.h>
+#include "filesystem.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+void SelectGroup( EMatchmakingGroupType eGroup, bool bSelected );
+
+#ifdef _DEBUG
+ #define GCMATCHMAKING_DEBUG_LEVEL 4
+#else
+ #define GCMATCHMAKING_DEBUG_LEVEL 1
+#endif
+#define GCMatchmakingDebugSpew( lvl, ...) do { if ( lvl <= GCMATCHMAKING_DEBUG_LEVEL ) { Msg( __VA_ARGS__); } } while(false)
+
+#define MM_REJOIN_WAIT_TIME 1.0f
+
+static const char* s_pszCasualCriteriaSaveFileName = "casual_criteria.vdf";
+
+
+// Ping stuff
+static ConVar tf_datacenter_ping_interval( "tf_datacenter_ping_interval", "180", FCVAR_DEVELOPMENTONLY );
+
+#ifdef TF_GC_PING_DEBUG
+ #define CV_TF_DATACENTER_PING_DEBUG_DEFAULT "1"
+#else
+ #define CV_TF_DATACENTER_PING_DEBUG_DEFAULT "0"
+#endif
+static ConVar tf_datacenter_ping_debug( "tf_datacenter_ping_debug", CV_TF_DATACENTER_PING_DEBUG_DEFAULT, FCVAR_INTERNAL_USE );
+
+static bool BPingDebug() { return tf_datacenter_ping_debug.GetBool(); }
+#define TFPingMsg(...) Msg("[SDR Ping] " __VA_ARGS__)
+#define TFPingDbg(...) if ( BPingDebug() ) { TFPingMsg( __VA_ARGS__ ); }
+
+// Allow disabling for staging. Will only send dummy values set by the overrides above
+#ifdef TF_GC_PING_DEBUG
+#include "tier0/icommandline.h"
+static bool BUseSteamDatagram() { return !CommandLine()->CheckParm("-nosteamdatagram" ); }
+#else
+static bool BUseSteamDatagram() { return true; }
+#endif
+
+using namespace GCSDK;
+
+// @FD We need this for TF?
+//DEFINE_LOGGING_CHANNEL_NO_TAGS( LOG_CONSOLE, "Console" );
+
+static CTFGCClientSystem s_TFGCClientSystem;
+CTFGCClientSystem *GTFGCClientSystem() { return &s_TFGCClientSystem; }
+
+// Dialog Prompt Asking users if they want to rejoin a MvM Game
+static CTFRejoinConfirmDialog *s_pRejoinLobbyDialog;
+
+//bool g_bClientReceivedGCWelcome = false;
+//bool CTFGCClientSystem::HasGCUserSessionBeenCreated() { return g_bClientReceivedGCWelcome; }
+
+//static ConVar tf_spectator_auto_spectate_games( "tf_spectator_auto_spectate_games", "0", 0, "Automatically spectate available games" );
+//static ConVar tf_auto_connect( "tf_auto_connect", "", 0, "Automatically connect to the specified server forever" );
+static ConVar tf_matchgroups( "tf_matchgroups", "0", FCVAR_ARCHIVE, "Bit masks of match groups to search in for matchmaking" );
+//static ConVar tf_auto_create_proxy( "tf_auto_create_proxy", "0", 0, "Automatically create a proxy" );
+//ConVar tf_debug_today_message_sorting( "tf_debug_today_message_sorting", "0", 0, "Print out unsorted and sorted today messages to the console" );
+
+#ifdef STAGING_ONLY
+CON_COMMAND( cl_check_process_count, "cl_check_process_count" )
+{
+ int iProcessCount = engine->GetInstancesRunningCount();
+ Msg( "cl_check_process_count - %d \n", iProcessCount );
+}
+#endif
+
+// This triggers a GC packet so isn't great to let clients misuse
+#if defined( STAGING_ONLY ) || defined( _DEBUG )
+CON_COMMAND( tf_datacenter_ping_refresh, "Force an immediate refresh of datacenter ping" )
+{
+ GTFGCClientSystem()->InvalidatePingData();
+}
+#endif // defined( STAGING_ONLY ) || defined( _DEBUG )
+
+CON_COMMAND( tf_datacenter_ping_dump, "Dump current datacenter ping values to console" )
+{
+ GTFGCClientSystem()->DumpPing();
+}
+
+static void OnRejoinMvMLobbyDialogCallBack( bool bConfirmed, void *pContext )
+{
+ GTFGCClientSystem()->RejoinLobby( bConfirmed );
+ s_pRejoinLobbyDialog = NULL;
+}
+
+void SubscribeToLocalPlayerSOCache( ISharedObjectListener* pListener )
+{
+ if ( steamapicontext && steamapicontext->SteamUser() )
+ {
+ CSteamID steamID = steamapicontext->SteamUser()->GetSteamID();
+ GCClientSystem()->GetGCClient()->AddSOCacheListener( steamID, pListener );
+ }
+ else
+ {
+ Assert( !"Failed to subscribe to local user's SOCache!" );
+ }
+}
+
+// Helper to add or replace a ping entry in an update
+static void ApplyPingToMsg( CMsgGCDataCenterPing_Update &msg, const CMsgGCDataCenterPing_Update_PingEntry &entry )
+{
+ // Existing?
+ const char *pszName = entry.name().c_str();
+ CMsgGCDataCenterPing_Update_PingEntry *pEntry = NULL;
+ for ( int j = 0; j < msg.pingdata_size(); ++j )
+ {
+ CMsgGCDataCenterPing_Update_PingEntry& existingEntry = *msg.mutable_pingdata(j);
+
+ if ( V_stricmp( existingEntry.name().c_str(), pszName ) == 0 )
+ {
+ pEntry = &existingEntry;
+ break;
+ }
+ }
+
+ // New?
+ if ( !pEntry )
+ {
+ pEntry = msg.add_pingdata();
+ }
+
+ pEntry->CopyFrom( entry );
+}
+
+const char *CTFGCClientSystem::k_pszSteamLobbyKey_PartyID = "PartyID";
+
+//-----------------------------------------------------------------------------
+// Reliable messages
+//-----------------------------------------------------------------------------
+class ReliableMsgNotificationAcknowledge : public CJobReliableMessageBase < ReliableMsgNotificationAcknowledge,
+ CMsgNotificationAcknowledge,
+ k_EMsgGC_NotificationAcknowledge,
+ CMsgNotificationAcknowledgeReply,
+ k_EMsgGC_NotificationAcknowledgeReply >
+{
+public:
+ const char *MsgName() { return "NotificationAcknowledge"; }
+ void InitDebugString( CUtlString &dbgStr )
+ {
+ dbgStr.Format( "Account %u / Notification %016llx",
+ Msg().Body().account_id(), Msg().Body().notification_id() );
+ }
+};
+
+CTFGCClientSystem::CTFGCClientSystem()
+: m_pPendingCreateOrUpdatePartyMsg( NULL )
+, m_flSendPartyUpdateMessageTime( FLT_MAX )
+, m_nMostSearchedMapCount( 0 )
+, m_WorldStatus()
+, m_bRegisteredSharedObjects( false )
+, m_bInittedGC( false )
+, m_eAcceptInviteStep( eAcceptInviteStep_None )
+, m_eCreateLobbyStatus( k_EResultOK )
+, m_bWantToActivateInviteUI( false )
+, m_steamIDGCAssignedMatch()
+, m_bAssignedMatchEnded( false )
+, m_eAssignedMatchGroup( k_nMatchGroup_Invalid )
+, m_uAssignedMatchID( 0 )
+, m_bServerAssignmentChanged( false )
+, m_rtLastPingFix( 0 )
+, m_bPendingPingRefresh( false )
+, m_bSentInitialPingFix( false )
+, m_flCheckForRejoinTime( 0 )
+, m_pSOCache( NULL )
+, m_eConnectState( eConnectState_Disconnected )
+, m_bGCUserSessionCreated( false )
+, m_bUserWantsToBeInMatchmaking( false )
+, m_nPendingAutoJoinPartyID( 0 )
+, m_eLocalWizardStep( TF_Matchmaking_WizardStep_INVALID )
+, m_callbackSteamLobbyCreated( this, &CTFGCClientSystem::OnSteamLobbyCreated )
+, m_callbackSteamLobbyEnter( this, &CTFGCClientSystem::OnSteamLobbyEnter )
+, m_callbackSteamLobbyChatMsg( this, &CTFGCClientSystem::OnSteamLobbyChatMsg )
+, m_callbackSteamGameLobbyJoinRequested( this, &CTFGCClientSystem::OnSteamGameLobbyJoinRequested )
+, m_callbackSteamLobbyDataUpdate( this, &CTFGCClientSystem::OnSteamLobbyDataUpdate )
+, m_callbackSteamLobbyChatUpdate( this, &CTFGCClientSystem::OnSteamLobbyChatUpdate )
+{
+ // replace base GCClientSystem
+ SetGCClientSystem( this );
+
+ s_pRejoinLobbyDialog = NULL;
+
+// if ( g_bClientReceivedGCWelcome )
+// {
+// Msg( "CTFGCClientSystem::CTFGCClientSystem firing event\n" );
+//
+// IGameEvent *pEvent = gameeventmanager->CreateEvent( "gc_user_session_created" );
+// if ( pEvent )
+// {
+// gameeventmanager->FireEventClientSide( pEvent );
+// }
+// }
+// else
+// {
+// Msg( "CTFGCClientSystem::CTFGCClientSystem user session not yet created\n" );
+// }
+}
+
+
+CTFGCClientSystem::~CTFGCClientSystem( void )
+{
+ // Prevent other system from using this pointer after it's destroyed
+ SetGCClientSystem( NULL );
+}
+
+////-----------------------------------------------------------------------------
+//// Purpose: Asynchronous job for getting news
+////-----------------------------------------------------------------------------
+//class CGCClientJobGetNews : public GCSDK::CGCClientJob
+//{
+//public:
+// CGCClientJobGetNews( GCSDK::CGCClient *pGCClient, int nAppID ) : GCSDK::CGCClientJob( pGCClient )
+// {
+// m_nAppID = nAppID;
+// }
+//
+// virtual bool BYieldingRunJob( void *pvStartParam )
+// {
+// CGCMsg<MsgGCGetNews_t> msgGetNews( k_EMsgGCGetNews );
+// msgGetNews.Body().m_unAppID = m_nAppID;
+//
+// GCSDK::CGCMsg<MsgGCNewsReponse_t> msgResponse( k_EMsgGCNewsResponse );
+// bool bRet = BYldSendMessageAndGetReply( msgGetNews, 150, &msgResponse, k_EMsgGCNewsResponse );
+// //Assert( bRet );
+//
+// if ( !bRet )
+// {
+// Warning( "CGCClientJobGetNews failed to get reply\n" );
+// GTFGCClientSystem()->SetGetNewsTime( Plat_FloatTime() + 5.0f );
+// return false;
+// }
+//
+// //deserialize KV
+// CUtlBuffer bufNews;
+// bufNews.Put( msgResponse.PubReadCur(), msgResponse.Body().m_cMsgLen );
+//
+// KVPacker packer;
+// KeyValues *pNewsKeys = GTFGCClientSystem()->GetNewsKeys();
+// pNewsKeys->Clear();
+// if ( !packer.ReadAsBinary( pNewsKeys, bufNews ) )
+// {
+// Warning( "Failed to deserialize key values from news request\n" );
+// return false;
+// }
+//
+// //KeyValuesDumpAsDevMsg( pNewsKeys );
+//
+// IGameEvent *pEvent = gameeventmanager->CreateEvent( "news_updated" );
+// if ( pEvent )
+// {
+// gameeventmanager->FireEventClientSide( pEvent );
+// }
+//
+// return true;
+// }
+//
+//private:
+// int m_nAppID;
+//};
+
+
+////-----------------------------------------------------------------------------
+//// Purpose: Asynchronous job for pinging the GC with a hello until we get
+//// a welcome
+////-----------------------------------------------------------------------------
+//class CGCClientJobHello : public GCSDK::CGCClientJob
+//{
+//public:
+// CGCClientJobHello( GCSDK::CGCClient *pGCClient )
+// : GCSDK::CGCClientJob( pGCClient )
+// {
+// }
+//
+// virtual bool BYieldingRunJob( void *pvStartParam )
+// {
+// CProtoBufMsg<CMsgClientHello> msg( k_EMsgGCClientHello );
+// msg.Body().set_version( engine->GetClientVersion() );
+//
+// while ( !g_bClientReceivedGCWelcome )
+// {
+// // Wait two seconds between messages
+// BYieldingWaitTime( 2 * k_nMillion );
+//
+// if ( !m_pGCClient->BSendMessage( msg ) )
+// return false;
+// }
+// return true;
+// }
+//};
+
+
+////-----------------------------------------------------------------------------
+//class CGCClientJobFindSourceTVGamesAutoSpectate : public GCSDK::CGCClientJob
+//{
+//public:
+// CGCClientJobFindSourceTVGamesAutoSpectate( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient )
+// {
+// }
+//
+// virtual bool BYieldingRunJob( void *pvStartParam )
+// {
+// CProtoBufMsg<CMsgFindSourceTVGames> msg( k_EMsgGCFindSourceTVGames );
+// CProtoBufMsg<CMsgSourceTVGamesResponse> msgResponse( k_EMsgGCSourceTVGamesResponse );
+//
+// static ConVarRef sv_search_key("sv_search_key");
+// if ( sv_search_key.IsValid() && *sv_search_key.GetString() )
+// {
+// msg.Body().set_search_key( sv_search_key.GetString() );
+// }
+//
+// msg.Body().set_start( 0 );
+// msg.Body().set_num_games( 10 );
+//
+// bool bRet = BYldSendMessageAndGetReply( msg, 15, &msgResponse, k_EMsgGCSourceTVGamesResponse );
+//
+// GTFGCClientSystem()->SetAutoSpectateCheckTime( Plat_FloatTime() + 30.0f ); // try again in 30 seconds
+//
+// if ( !bRet )
+// {
+// Warning( "CGCClientJobFindSourceTVGamesDebug failed to get reply\n" );
+// return false;
+// }
+//
+// if ( GTFGCClientSystem()->GetSignonState() != SIGNONSTATE_NONE || !msgResponse.Body().games_size() )
+// {
+// return true; // already connected somewhere else
+// }
+//
+// const CSourceTVGame &game = msgResponse.Body().games( RandomInt( 0, msgResponse.Body().games_size() - 1 ));
+//
+// GTFGCClientSystem()->StartWatchingGame( game.server_steamid() );
+// return true;
+// }
+//
+//private:
+//};
+
+void CTFGCClientSystem::LoadCasualSearchCriteria()
+{
+ // Read casual criteria if the file exists
+ CUtlBuffer buffer;
+ buffer.SetBufferType( true, true );
+ if ( g_pFullFileSystem->ReadFile( s_pszCasualCriteriaSaveFileName, NULL, buffer ) &&
+ buffer.TellPut() > buffer.TellGet() )
+ {
+ // Null terminate. Why is buffer this pseudo-text class but has AddNullTerminator private?
+ const char zero = '\0';
+ buffer.Put( &zero, sizeof( zero ) );
+
+ std::string strIn( (const char *)buffer.PeekGet() );
+
+ google::protobuf::TextFormat::ParseFromString( strIn, m_msgLocalSearchCriteria.mutable_casual_criteria() );
+
+ // let the CCasualCriteriaHelper validate/cleanup the bits that we've just loaded
+ CCasualCriteriaHelper casualHelper( m_msgLocalSearchCriteria.casual_criteria() );
+ if ( GetParty() != NULL )
+ {
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->mutable_search_criteria()->mutable_casual_criteria()->CopyFrom( casualHelper.GetCasualCriteria() );
+ }
+
+ m_msgLocalSearchCriteria.mutable_casual_criteria()->CopyFrom( casualHelper.GetCasualCriteria() );
+
+ FireGameEventPartyUpdated();
+ }
+ else
+ {
+ // default to the Core maps
+ SelectGroup( kMatchmakingType_Core, true );
+ }
+}
+
+// Initialize steam client datagram lib if we haven't already
+static bool CheckInitSteamDatagramClientLib()
+{
+ if ( !BUseSteamDatagram() )
+ return false;
+
+ if ( !steamapicontext || !steamapicontext->SteamHTTP() || !steamapicontext->SteamUtils() )
+ {
+ Assert( false );
+ Warning( "Steam datagram not initialized - no Steam context\n" );
+ return false;
+ }
+
+ static bool bInittedNetwork = false;
+ if ( bInittedNetwork )
+ return true;
+
+ // Locate the first PLATFORM path
+ char szAbsPlatform[MAX_PATH] = "";
+ const char *pszConfigDir = "config";
+ g_pFullFileSystem->GetSearchPath( "PLATFORM", false, szAbsPlatform, sizeof(szAbsPlatform) );
+
+ char *semi = strchr( szAbsPlatform, ';' );
+ if ( semi )
+ *semi = '\0';
+
+ char szAbsConfigDir[MAX_PATH];
+ V_ComposeFileName( szAbsPlatform, pszConfigDir, szAbsConfigDir, sizeof(szAbsConfigDir) );
+ SteamDatagramErrMsg errMsg;
+ if ( !SteamDatagramClient_Init( szAbsConfigDir, k_ESteamDatagramPartner_Steam, (1<<k_ESteamDatagramPartner_Steam), errMsg ) )
+ {
+ Warning( "Failed to initialize steam datagram client. %s\n", errMsg );
+ return false;
+ }
+ bInittedNetwork = true;
+
+ return true;
+}
+bool CTFGCClientSystem::Init()
+{
+ // Get this guy created
+ GetMMDashboard();
+
+ // Convars may have initialized before us
+ UpdateCustomPingTolerance();
+
+ ListenForGameEvent( "client_disconnect" );
+ ListenForGameEvent( "client_beginconnect" );
+ ListenForGameEvent( "server_spawn" );
+
+ // init steamdatagram system ASAP so we're more likely to have initial ping data to the clusters ready by the time
+ // we ask for it
+ CheckInitSteamDatagramClientLib();
+ if ( SteamNetworkingUtils() )
+ SteamNetworkingUtils()->CheckPingDataUpToDate( 0.0f );
+
+ // Just loading the library starts initial pinging
+ m_bPendingPingRefresh = true;
+
+// m_GameVersion = GAME_VERSION_CURRENT;
+
+ // Default search criteria
+ //m_msgLocalSearchCriteria.set_matchgroups( 1 );
+ m_msgLocalSearchCriteria.set_matchmaking_mode( TF_Matchmaking_LADDER );
+
+ m_bLocalSquadSurplus = false;
+ return true;
+}
+
+
+void CTFGCClientSystem::PostInit()
+{
+ BaseClass::PostInit();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::PreInitGC()
+{
+ if ( !m_bRegisteredSharedObjects )
+ {
+// REG_SHARED_OBJECT_SUBCLASS( CTFHeroStandings );
+// REG_SHARED_OBJECT_SUBCLASS( CTFGameAccountClient );
+ REG_SHARED_OBJECT_SUBCLASS( CTFParty );
+ REG_SHARED_OBJECT_SUBCLASS( CTFGSLobby );
+ REG_SHARED_OBJECT_SUBCLASS( CPartyInvite );
+// REG_SHARED_OBJECT_SUBCLASS( CTFBetaParticipation );
+ REG_SHARED_OBJECT_SUBCLASS( CTFPartyInvite );
+ REG_SHARED_OBJECT_SUBCLASS( CTFRatingData );
+
+ m_bRegisteredSharedObjects = true;
+ }
+
+// if ( m_flGetNewsTime == 0.0f )
+// {
+// m_flGetNewsTime = Plat_FloatTime() + 2.0f;
+// }
+}
+
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::PostInitGC()
+{
+ GCMatchmakingDebugSpew( 1, "CTFGCClientSystem::PostInitGC\n" );
+
+ if ( steamapicontext && steamapicontext->SteamUser() )
+ {
+ GCMatchmakingDebugSpew( 1, "CTFGCClientSystem - adding listener\n" );
+
+ CSteamID steamID = steamapicontext->SteamUser()->GetSteamID();
+ GCClientSystem()->FindOrAddSOCache( steamID )->AddListener( this );
+ }
+ else
+ {
+ Warning( "CTFGCClientSystem - couldn't add listener because Steam wasn't ready\n" );
+ }
+
+// @FD We need this?
+// // Force a resend of our SO cache.
+// // This is only necessary because the Steam client doesn't detect a quick relaunch of the game, so the GC doesn't get a SessionStartPlaying call.
+// CProtoBufMsg<CMsgForceSOCacheResend> msg( k_EMsgForceSOCacheResend );
+// GCClientSystem()->BSendMessage( msg );
+
+// // Start hello job to ping the GC until we get a welcome
+// if ( !g_bClientReceivedGCWelcome )
+// {
+// CGCClientJobHello *pJob = new CGCClientJobHello( GCClientSystem()->GetGCClient() );
+// pJob->StartJob( NULL );
+// }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose:
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::ReceivedClientWelcome( const CMsgClientWelcome &msg )
+{
+ BaseClass::ReceivedClientWelcome( msg );
+
+ // Send a client init message in response to welcome
+ GCSDK::CProtoBufMsg<CMsgTFClientInit> initMsg( k_EMsgGC_TFClientInit );
+ initMsg.Body().set_client_version( engine->GetClientVersion() );
+ char uilanguage[ 64 ];
+ engine->GetUILanguage( uilanguage, sizeof( uilanguage ) );
+ initMsg.Body().set_language( PchLanguageToELanguage( uilanguage ) );
+ this->BSendMessage( initMsg );
+
+ // Send a ping fix if we know it (e.g. we re-connected to the GC, or got a fix before the GC was ready)
+ if ( BHavePingData() )
+ {
+ GCSDK::CProtoBufMsg<CMsgGCDataCenterPing_Update> pingmsg( k_EMsgGCDataCenterPing_Update );
+ pingmsg.Body().CopyFrom( GetPingData() );
+
+ m_bSentInitialPingFix = this->BSendMessage( pingmsg );
+ }
+ else
+ {
+ // PingThink will send it next fix
+ m_bSentInitialPingFix = false;
+ }
+}
+
+class CSendCreateOrUpdatePartyMsgJob;
+
+static int s_nNumWizardStepChangesWaitingForReply = 0;
+
+class CSendCreateOrUpdatePartyMsgJob : public GCSDK::CGCClientJob
+{
+public:
+
+ CSendCreateOrUpdatePartyMsgJob()
+ : GCSDK::CGCClientJob( GCClientSystem()->GetGCClient() )
+ , msg( k_EMsgGCCreateOrUpdateParty )
+ {
+ msg.Body().set_client_version( engine->GetClientVersion() );
+ }
+
+ CProtoBufMsg<CMsgCreateOrUpdateParty> msg;
+ CProtoBufMsg<CMsgCreateOrUpdatePartyReply> msgReply;
+
+ static void UpdateWizardStepFromParty()
+ {
+ // Make sure we have a party and the data changed
+ CTFParty *pParty = GTFGCClientSystem()->GetParty();
+ if ( pParty != NULL && GTFGCClientSystem()->m_eLocalWizardStep != pParty->Obj().wizard_step() )
+ {
+ GTFGCClientSystem()->m_eLocalWizardStep = pParty->Obj().wizard_step();
+ GTFGCClientSystem()->FireGameEventPartyUpdated();
+ }
+ }
+
+ virtual bool BYieldingRunJob( void *pvStartParam )
+ {
+ Assert( s_nNumWizardStepChangesWaitingForReply >= 0 );
+ if ( msg.Body().has_wizard_step() )
+ ++s_nNumWizardStepChangesWaitingForReply;
+
+ bool bGotReply = BYldSendMessageAndGetReply( msg, 10, &msgReply, k_EMsgGCCreateOrUpdatePartyReply );
+
+ if ( msg.Body().has_wizard_step() )
+ --s_nNumWizardStepChangesWaitingForReply;
+ Assert( s_nNumWizardStepChangesWaitingForReply >= 0 );
+
+ if ( !bGotReply )
+ {
+ CTFParty *pParty = GTFGCClientSystem()->GetParty();
+ if ( pParty )
+ {
+ pParty->SetOffline( true );
+ }
+
+ GTFGCClientSystem()->FireGameEventPartyUpdated();
+ // !FIXME! Here we really should mark the GC Client as not
+ // being connected to the GC
+
+ Warning( "Timed out getting reply from GC to change party.\n" );
+ return true;
+ }
+
+ // Any error message?
+ EResult result = (EResult)msgReply.Body().result();
+ const char *pszMsg = msgReply.Body().message().c_str();
+ if ( *pszMsg != '\0' )
+ {
+ if ( result != k_EResultOK )
+ {
+ Warning( "%s\n", pszMsg );
+ }
+ else
+ {
+ Msg( "%s\n", pszMsg );
+ }
+ }
+
+ // Check for error.
+ switch ( result )
+ {
+ case k_EResultOK:
+ break;
+ case k_EResultInvalidProtocolVer:
+ //if ( GTFGCClientSystem()->BIsPartyLeader() )
+ //{
+ //}
+ GTFGCClientSystem()->EndMatchmaking();
+ ShowMessageBox( "#TF_MM_NotCurrentVersionTitle", "#TF_MM_NotCurrentVersionMessage", "#GameUI_OK" );
+ return true;
+ default:
+ Warning( "CreateOrUpdate returned error code %d\n", result );
+ break;
+ }
+
+ // If no more messages pending that will change the wizard step, then
+ // get in sync with the GC
+ if ( s_nNumWizardStepChangesWaitingForReply <= 0 )
+ {
+ UpdateWizardStepFromParty();
+ GTFGCClientSystem()->FireGameEventPartyUpdated();
+ return true;
+ }
+
+ // Did we request a particular wizard step?
+ if ( !msg.Body().has_wizard_step() )
+ return true;
+
+ // Do we have a party?
+ CTFParty *pParty = GTFGCClientSystem()->GetParty();
+ if ( pParty == NULL )
+ return true;
+
+ // We got a response. Definitely not offline anymore
+ pParty->SetOffline( false );
+
+ // We should be the party leader
+ Assert( GTFGCClientSystem()->BIsPartyLeader() );
+
+ // Party should have a known wizard step
+ Assert( pParty->Obj().has_wizard_step() );
+ if ( !pParty->Obj().has_wizard_step() )
+ return true;
+
+ // If GC did not like our request, or we are starting or stopping searching,
+ // the force us to get on the same page as the GC
+ if (
+ msg.Body().wizard_step() != pParty->Obj().wizard_step() // after processing message, we are not in requested step
+ || msg.Body().wizard_step() == TF_Matchmaking_WizardStep_SEARCHING // we requested to search
+ || pParty->Obj().wizard_step() == TF_Matchmaking_WizardStep_SEARCHING // we are now searching
+ || GTFGCClientSystem()->m_eLocalWizardStep == TF_Matchmaking_WizardStep_SEARCHING // we think we were searching previously
+ )
+ {
+ UpdateWizardStepFromParty();
+ }
+
+ return true;
+ }
+};
+
+CMsgCreateOrUpdateParty *CTFGCClientSystem::GetCreateOrUpdatePartyMsg()
+{
+ // TODO We should only send updates if something changes, some callers might just be copying same-values back in :-/
+
+ if ( m_pPendingCreateOrUpdatePartyMsg == NULL )
+ {
+ m_pPendingCreateOrUpdatePartyMsg = new CSendCreateOrUpdatePartyMsgJob;
+ }
+
+ if ( m_flSendPartyUpdateMessageTime == FLT_MAX )
+ {
+ if ( GetParty() && GetParty()->GetNumMembers() > 1 )
+ {
+ // If we're in a party, delay the sending of the message to queue up any rapid changes
+ // that might occur from users clicking on criteria UI controls
+ m_flSendPartyUpdateMessageTime = Plat_FloatTime() + 2.f;
+ }
+ else
+ {
+ m_flSendPartyUpdateMessageTime = 0.f;
+ }
+ }
+
+ return &m_pPendingCreateOrUpdatePartyMsg->msg.Body();
+}
+
+
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::LevelShutdownPostEntity()
+{
+ BaseClass::LevelShutdownPostEntity();
+// // clear caches, so the player will see his stats update after a game
+// if ( Dashboard() )
+// {
+// Dashboard()->ClearDashboardCaches();
+// }
+}
+
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::LevelInitPreEntity()
+{
+ BaseClass::LevelInitPreEntity();
+}
+
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::Shutdown()
+{
+ GCMatchmakingDebugSpew( 1, "CTFGCClientSystem::ShutdownGC\n" );
+
+ if ( steamapicontext && steamapicontext->SteamUser() )
+ {
+ GCMatchmakingDebugSpew( 1, "CTFGCClientSystem - adding listener\n" );
+
+ CSteamID steamID = steamapicontext->SteamUser()->GetSteamID();
+ GCSDK::CGCClientSharedObjectCache *pSOCache = GCClientSystem()->GetSOCache( steamID );
+ Assert( pSOCache ); // we installed ourselves as a listener, right, so it shouldn't have deleted the cache
+ if ( pSOCache )
+ {
+ pSOCache->RemoveListener( this );
+ }
+ }
+ else
+ {
+ Warning( "CTFGCClientSystem - couldn't add listener because Steam wasn't ready\n" );
+ }
+
+ BaseClass::Shutdown();
+
+ SteamDatagramClient_Kill();
+}
+
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::FireGameEvent( IGameEvent *event )
+{
+ const char *pEventName = event->GetName();
+ // Disconnected from gameserver
+ if ( !Q_stricmp( pEventName, "client_disconnect" ) )
+ {
+ m_steamIDCurrentServer.Clear();
+
+ // Do not send end match making if we see the mvm end message
+ if ( !Q_stricmp( event->GetString( "message", "" ), "#TF_PVE_Disconnect" ) )
+ return;
+
+ // Don't bail if GC has told us to expect to be put into a new party
+ if ( m_nPendingAutoJoinPartyID != 0 )
+ return;
+
+ m_eConnectState = eConnectState_Disconnected; // clear variable first to avoid recursion
+
+ // Ladder games
+ //if ( !Q_stricmp( event->GetString( "message", "" ), "#TF_Competitive_Disconnect" ) ) // FIXME only disconnect if we were previously connected(ing), this fires spuriously from the main menu
+ //{
+ // engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby ladder" );
+ // return;
+ //}
+
+ CTFParty *pParty = GetParty();
+
+ // Return to party screen upon disconnect
+ if ( m_bUserWantsToBeInMatchmaking && ( ( pParty && pParty->GetNumMembers() > 1 ) || m_eLocalWizardStep == TF_Matchmaking_WizardStep_SEARCHING ) )
+ {
+ switch( GTFGCClientSystem()->GetSearchMode() )
+ {
+ case TF_Matchmaking_LADDER:
+ engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby ladder" );
+ break;
+
+ case TF_Matchmaking_CASUAL:
+ engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby casual" );
+ break;
+ case TF_Matchmaking_MVM:
+ engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby mvm" );
+ break;
+ default:
+ engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby" );
+ Assert( !"Unhandled enum value" );
+ break;
+ };
+ }
+
+ return;
+ }
+
+ // Started attempting connection to gameserver
+ if ( !Q_stricmp( pEventName, "client_beginconnect" ) )
+ {
+ Assert( IsConnectStateDisconnected() );
+
+ // TODO does the retry command set this source? It should go through ::ConnectToServer
+ const char *pszSource = event->GetString( "source", "" );
+ if ( FStrEq( pszSource, "matchmaking" ) )
+ {
+ // Assume we're doing the right thing until we hit server_spawn and figure otherwise.
+ m_steamIDCurrentServer = m_steamIDGCAssignedMatch;
+ m_eConnectState = eConnectState_ConnectingToMatchmade;
+ }
+ else
+ {
+ if ( !BAllowMatchMakingInGame() )
+ {
+ EndMatchmaking();
+ }
+
+ m_eConnectState = eConnectState_NonmatchmadeServer;
+ }
+ return;
+ }
+
+ // Successfully connected to a gameserver. For MM purposes, we stay in state connecting until server spawn as that
+ // ensures there's no intermediate "loading into some server but we're not sure of its steamid yet" state.
+ if ( !Q_strcmp( pEventName, "server_spawn" ) )
+ {
+ GCMatchmakingDebugSpew( 4, "Client reached server_spawn.\n" );
+ switch ( m_eConnectState )
+ {
+ default:
+ AssertMsg1( false, "Unknown connect state %d", m_eConnectState );
+ // These two can happen when doing weird things with timedemo or listen servers
+ case eConnectState_Disconnected:
+ m_eConnectState = eConnectState_NonmatchmadeServer;
+ GCMatchmakingDebugSpew( 4, "Client connected to non-matchmade.\n" );
+ break;
+
+ case eConnectState_ConnectingToMatchmade:
+ m_eConnectState = eConnectState_ConnectedToMatchmade;
+ GCMatchmakingDebugSpew( 4, "Client connected to matchmade.\n" );
+ break;
+
+ case eConnectState_ConnectedToMatchmade:
+ break;
+
+ case eConnectState_NonmatchmadeServer:
+ break;
+ }
+ m_steamIDCurrentServer.Clear();
+ if ( steamapicontext && steamapicontext->SteamUser() && steamapicontext->SteamUtils() )
+ {
+ m_steamIDCurrentServer.SetFromString( event->GetString( "steamid", "" ), GetUniverse() );
+ GCMatchmakingDebugSpew( 4, "Recognizing MM server id %s\n", m_steamIDCurrentServer.Render() );
+ }
+
+ if ( m_eConnectState == eConnectState_ConnectedToMatchmade && !m_steamIDCurrentServer.IsValid() )
+ {
+ Warning( "Connected to MM server but no GS steamid is set.\n" );
+ }
+
+ return;
+ }
+
+}
+
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::InvalidatePingData()
+{
+ // Invalidate current refresh
+ TFPingMsg("Forcing ping refresh\n" );
+ m_bPendingPingRefresh = true;
+
+ // Wipe all cached data.
+ m_rtLastPingFix = 0; // 0 means never. Or time traveler. 50/50.
+ m_msgCachedPingUpdate = CMsgGCDataCenterPing_Update();
+
+ if ( BUseSteamDatagram() && SteamNetworkingUtils() )
+ {
+ SteamNetworkingUtils()->CheckPingDataUpToDate( 0.0f );
+ }
+}
+
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::PingThink()
+{
+ ISteamNetworkingUtils *pUtils = SteamNetworkingUtils();
+ if ( !pUtils && BUseSteamDatagram() )
+ {
+ Assert( pUtils );
+ return;
+ }
+
+ if ( !m_bPendingPingRefresh )
+ {
+ // No refresh in progress, start one if necessary
+ RTime32 rtRefreshInterval = (RTime32)Clamp( tf_datacenter_ping_interval.GetInt(), 0, INT32_MAX );
+ RTime32 rtLastRefreshAge = CRTime::RTime32TimeCur() - m_rtLastPingFix;
+
+ if ( rtLastRefreshAge <= rtRefreshInterval )
+ { return; }
+
+ // Start a refresh
+ m_bPendingPingRefresh = true;
+
+ // Non-steam datagram will just succeed next heartbeat
+ if ( BUseSteamDatagram() )
+ { pUtils->CheckPingDataUpToDate(0.0f); }
+
+ return;
+ }
+
+ //
+ // Refresh pending, speculatively start building an update and bail out if we find missing data
+ //
+
+ CMsgGCDataCenterPing_Update newPingUpdate;
+
+ // Check if our connection status is good enough for ping measurement
+ // Without steamdatagram, we'll just always succeed immediately with empty data (plus overrides below)
+ if ( BUseSteamDatagram() )
+ {
+ // Not ready yet?
+ if ( pUtils->IsPingMeasurementInProgress() )
+ return;
+
+ // Get complete list of points of presence
+ CUtlVector<SteamNetworkingPOPID> vecPoPs;
+ vecPoPs.SetCount( pUtils->GetPOPCount() );
+ vecPoPs.SetCountNonDestructively( pUtils->GetPOPList( vecPoPs.Base(), vecPoPs.Count() ) );
+
+ // Waiting on a ping refresh to complete. Check if we have every datacenter and cache off if so.
+ //
+ // NOTE that we don't use SDR for actual-routing yet, so we are purposefully using the *router* ping values as
+ // estimates for that DC -- since we will talk to the DC directly and not via the relay.
+ for ( SteamNetworkingPOPID id: vecPoPs )
+ {
+ char szCode[ 8 ];
+ GetSteamNetworkingLocationPOPStringFromID( id, szCode );
+
+ CMsgGCDataCenterPing_Update_PingEntry *pMsgPingEntry = newPingUpdate.add_pingdata();
+ pMsgPingEntry->set_name( szCode );
+ int nPing = pUtils->GetDirectPingToPOP( id );
+ if ( nPing >= 0 )
+ {
+ pMsgPingEntry->set_ping( nPing );
+ }
+ else
+ {
+ nPing = pUtils->GetPingToDataCenter( id, nullptr );
+ if ( nPing >= 0 )
+ {
+ pMsgPingEntry->set_ping( nPing );
+ pMsgPingEntry->set_ping_status( CMsgGCDataCenterPing_Update_Status_FallbackToDCPing );
+ }
+ }
+
+ if ( !pMsgPingEntry->has_ping() )
+ {
+ pMsgPingEntry->set_ping_status( CMsgGCDataCenterPing_Update_Status_Unreachable );
+ }
+ }
+ }
+ else
+ {
+ // Otherwise we're fine
+ TFPingMsg( "Not using steam datagram, proceeding with empty cluster ping data\n" );
+ }
+
+ // If we're in beta/dev, add the magic "beta" cluster. See tf_datacenter_info on GC.
+ EUniverse eUniverse = GetUniverse();
+ if ( eUniverse == k_EUniverseBeta || eUniverse == k_EUniverseDev )
+ {
+ CMsgGCDataCenterPing_Update_PingEntry newEntry;
+ newEntry.set_name( "beta" );
+ newEntry.set_ping( 5 );
+ newEntry.set_ping_status( CMsgGCDataCenterPing_Update_Status_Normal );
+ ApplyPingToMsg( newPingUpdate, newEntry );
+ }
+
+#ifdef TF_GC_PING_DEBUG
+ // Apply overrides
+ for ( int j = 0; j < m_msgPingOverrides.pingdata_size(); ++j )
+ {
+ ApplyPingToMsg( newPingUpdate, m_msgPingOverrides.pingdata(j) );
+ }
+#endif // def TF_GC_PING_DEBUG
+
+ // We made it through all routers without bailing, can claim to have ping data now
+ if ( BConnectedtoGC() )
+ {
+ GCSDK::CProtoBufMsg<CMsgGCDataCenterPing_Update> msg( k_EMsgGCDataCenterPing_Update );
+ msg.Body().CopyFrom( newPingUpdate );
+
+ if ( this->BSendMessage( msg ) )
+ {
+ TFPingDbg( "Initial ping fix sent\n" );
+ m_bSentInitialPingFix = true;
+ }
+ }
+
+ m_bPendingPingRefresh = false;
+ m_rtLastPingFix = CRTime::RTime32TimeCur();
+ m_msgCachedPingUpdate = newPingUpdate;
+
+ IGameEvent *event = gameeventmanager->CreateEvent( "ping_updated" );
+ if ( event )
+ {
+ gameeventmanager->FireEventClientSide( event );
+ }
+
+ if ( BPingDebug() )
+ {
+ DumpPing();
+ }
+}
+
+#ifdef TF_GC_PING_DEBUG
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::SetPingOverride( const char *pszDataCenter, uint32 nPing, CMsgGCDataCenterPing_Update_Status eStatus )
+{
+ CMsgGCDataCenterPing_Update_PingEntry newEntry;
+ newEntry.set_name( pszDataCenter );
+ newEntry.set_ping( nPing );
+ newEntry.set_ping_status( eStatus );
+ ApplyPingToMsg( m_msgPingOverrides, newEntry );
+
+ InvalidatePingData();
+}
+
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::ClearPingOverrides()
+{
+ m_msgPingOverrides.clear_pingdata();
+
+ InvalidatePingData();
+}
+#endif // def TF_GC_PING_DEBUG
+
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::Update( float frametime )
+{
+ BaseClass::Update( frametime );
+
+// if ( m_flGetNewsTime != 0.0f && Plat_FloatTime() > m_flGetNewsTime && steamapicontext && steamapicontext->SteamUtils() )
+// {
+// m_flGetNewsTime = 0.0f;
+//
+// // get the latest news
+// CGCClientJobGetNews *pJob = new CGCClientJobGetNews( GCClientSystem()->GetGCClient(), (int) engine->GetAppID() );
+// pJob->StartJob( NULL );
+// }
+
+ PingThink();
+
+ // Check if it's time to send a party update message
+ if ( Plat_FloatTime() > m_flSendPartyUpdateMessageTime )
+ {
+ m_flSendPartyUpdateMessageTime = FLT_MAX;
+
+ Assert( m_pPendingCreateOrUpdatePartyMsg );
+ if ( m_pPendingCreateOrUpdatePartyMsg )
+ {
+ // Send the message
+ m_pPendingCreateOrUpdatePartyMsg->StartJob( NULL );
+ m_pPendingCreateOrUpdatePartyMsg = NULL;
+ }
+ }
+
+
+ CTFParty *pParty = GetParty();
+ CTFGSLobby *pLobby = GetLobby();
+
+ // Are we in a active lobby?
+ bool bInLiveMatch = BConnectedToMatchServer( true );
+ // If we do, are we actively connected to said match?
+ bool bHaveLiveMatch = BHaveLiveMatch();
+ bool bNewServerAssignment = m_bServerAssignmentChanged;
+ m_bServerAssignmentChanged = false;
+
+ Assert( !bInLiveMatch || m_steamIDCurrentServer.IsValid() );
+
+
+ if ( bInLiveMatch )
+ {
+ // We cannot assume cannot assume the gc will tell of us the match ending -- the GC connection is fallible, and
+ // the gameserver is authoritative once we're assigned (as long as the GC doesn't revoke said assignment, see
+ // SOChanged)
+ CTFGameRules *pTFGameRules = TFGameRules();
+ // If we're not loaded enough to look at gamerules, assume the match is live until we reach that state.
+ //
+ // - Because source engine, we can get a stale TFGameRules from our *last* game *after* starting a new
+ // connection. Only even think about asking once our connect state hits connected (keyed to server_spawn)
+ if ( m_eConnectState == eConnectState_ConnectedToMatchmade &&
+ engine->IsInGame() &&
+ pTFGameRules && pTFGameRules->RecievedBaseline() && pTFGameRules->IsManagedMatchEnded() )
+ {
+ // We no longer consider this our assigned match. Only the GC can change the GCAssignedMatch, this bool is
+ // our "but we reject this". SOChanged will clear it if a new assignment overrides things.
+ GCMatchmakingDebugSpew( 1, "GS marked assigned match as ended\n" );
+ m_bAssignedMatchEnded = true;
+ }
+ }
+
+ if ( !bHaveLiveMatch )
+ {
+ // Are we waiting to activate the lobby UI until a certain party appears?
+ if ( m_nPendingAutoJoinPartyID != 0 && pParty != NULL && pParty->GetGroupID() == m_nPendingAutoJoinPartyID )
+ {
+ Msg( "New party was instanced that GC told us to expect. Entering matchmaking lobby UI\n" );
+ engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby ladder" );
+ BeginMatchmaking( pParty->GetMatchmakingMode() );
+ }
+
+ /// XXX(JohnS): Right now invites just wont work if you're in a match, needs better flow.
+
+ // Do we have a pending invite we need to process?
+ if ( m_eAcceptInviteStep == eAcceptInviteStep_ReadyToJoinSteamLobby )
+ {
+
+ // Wait for everything else to go away, if we have anything
+ if ( !IsConnectStateDisconnected() )
+ {
+ //Msg( "Disconnecting from current server to accept invite\n" );
+ engine->ClientCmd_Unrestricted( "disconnect" );
+ }
+ else if ( m_bUserWantsToBeInMatchmaking )
+ {
+ EndMatchmaking();
+ }
+ else if ( pLobby == NULL && GetParty() == NULL )
+ {
+ Assert( m_steamIDLobbyInviteAccepted.IsValid() );
+ m_eAcceptInviteStep = eAcceptInviteStep_JoinSteamLobby;
+
+ // OK, start joining the lobby.
+ Msg( "Joining lobby %s\n", m_steamIDLobbyInviteAccepted.Render() );
+ steamapicontext->SteamMatchmaking()->JoinLobby( m_steamIDLobbyInviteAccepted );
+ m_steamIDLobbyInviteAccepted = CSteamID();
+ }
+ }
+ }
+
+
+ if ( bHaveLiveMatch )
+ {
+ if ( m_eConnectState != eConnectState_Disconnected && engine->IsInGame() )
+ {
+ // The dashboard will handle this automatically
+ }
+ else if ( m_bUserWantsToBeInMatchmaking && bNewServerAssignment )
+ {
+ // Use the autojoin if in the MM flow and this is a fresh match
+ m_AutoJoinHandler.MatchFound();
+ }
+ else
+ {
+ // Use the prompt
+ m_PromptJoinHandler.MatchFound();
+ }
+ }
+
+ FOR_EACH_VEC_BACK( m_vecDelayedLocalPlayerSOListenersToAdd, i )
+ {
+ SubscribeToLocalPlayerSOCache( m_vecDelayedLocalPlayerSOListenersToAdd[ i ] );
+ m_vecDelayedLocalPlayerSOListenersToAdd.Remove( i );
+ }
+}
+
+void CTFGCClientSystem::SOCacheSubscribed( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent )
+{
+ if ( steamIDOwner == ClientSteamContext().GetLocalPlayerSteamID() )
+ {
+ // Assert( m_pSOCache == NULL ); // we *can* get two SOCacheSubscribed calls in a row.
+ m_pSOCache = GCClientSystem()->GetSOCache( steamIDOwner );
+ Assert( m_pSOCache != NULL );
+
+ if ( gameeventmanager )
+ {
+
+ // force a party/lobby update whenever our SO cache arrives
+ FireGameEventPartyUpdated();
+ FireGameEventLobbyUpdated();
+ }
+ }
+}
+
+void CTFGCClientSystem::FireGameEventPartyUpdated()
+{
+ IGameEvent *event = gameeventmanager->CreateEvent( "party_updated" );
+ if ( event )
+ {
+ gameeventmanager->FireEventClientSide( event );
+ }
+}
+
+void CTFGCClientSystem::FireGameEventLobbyUpdated()
+{
+ IGameEvent *event2 = gameeventmanager->CreateEvent( "lobby_updated" );
+ if ( event2 )
+ {
+ gameeventmanager->FireEventClientSide( event2 );
+ }
+}
+
+void CTFGCClientSystem::SOCreated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) { SOChanged( pObject, SOChanged_Create, eEvent ); }
+void CTFGCClientSystem::SOUpdated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) { SOChanged( pObject, SOChanged_Update, eEvent ); }
+void CTFGCClientSystem::SODestroyed( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent ) { SOChanged( pObject, SOChanged_Destroy, eEvent ); }
+
+void CTFGCClientSystem::SOChanged( const GCSDK::CSharedObject *pObject, SOChangeType_t changeType, GCSDK::ESOCacheEvent eEvent )
+{
+ // Broadcasts
+ if ( pObject->GetTypeID() == CTFParty::k_nTypeID )
+ {
+ #if GCMATCHMAKING_DEBUG_LEVEL > 0
+ switch ( changeType )
+ {
+ case SOChanged_Create: GCMatchmakingDebugSpew( 1, "Party created\n"); break;
+ case SOChanged_Update: GCMatchmakingDebugSpew( 2, "Party updated\n"); break;
+ case SOChanged_Destroy: GCMatchmakingDebugSpew( 1, "Party destroyed\n"); break;
+ default: AssertMsg1( false, "Bogus change type %d", changeType );
+ }
+ #endif
+
+ CTFParty *pParty = GetParty();
+ if ( changeType == SOChanged_Destroy )
+ {
+ Assert( pParty == NULL );
+ FireGameEventPartyUpdated();
+ }
+ else if ( pParty != NULL ) // FIXME we're restarting the game after a crash, rejoining a match, and have no wizard step (or other BeginMatchmaking()) setup, so when we return to UI it's state is running but fucked
+ {
+ if ( pParty->BOffline() )
+ {
+ pParty->SetOffline( false );
+ // The user says they dont want to be in matchmaking, but the party coming in says that its
+ // searching or hanging out in the UI, which is not what we're doing. This can happen in
+ // the following circumstances:
+ // 1) With the GC up, start searching for a match
+ // 2) Crash the GC
+ // 3) Go back to the main menu
+ // 4) Reboot the GC
+ if ( pParty->GetState() == CSOTFParty_State_UI || pParty->GetState() == CSOTFParty_State_FINDING_MATCH )
+ {
+ if ( !m_bUserWantsToBeInMatchmaking )
+ {
+ Msg( "Party was updated/created, but our party is marked offline, we don't want to be matchmaking, and the party is not in a match. Ending matchmaking\n" );
+ EndMatchmaking();
+ }
+ else
+ {
+ Msg( "Party was updated/created, and our party is marked offline, and the party is not in a match. Sending update to GC with our predicted changes\n" );
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->set_wizard_step( m_eLocalWizardStep );
+ }
+ }
+ }
+ else
+ {
+ bool bShouldGoToMMUI = false;
+
+ // We'll hit this when start the game back up after a crash
+ if ( !m_bUserWantsToBeInMatchmaking && ( eEvent == eSOCacheEvent_Subscribed || eEvent == eSOCacheEvent_ListenerAdded ) && m_eAcceptInviteStep != eAcceptInviteStep_JoinParty )
+ {
+ switch( pParty->GetState() )
+ {
+ case CSOTFParty_State_UI:
+ case CSOTFParty_State_FINDING_MATCH:
+ // They backed out of the MM UI somehow, and are getting party updates. We want out.
+ if ( pParty->GetNumMembers() <= 1 )
+ {
+
+ Msg( "Creating a party when we don't want to be in matchmaking, and we're the only ones in it. Possibly and old party from an old session. Ending matchmaking.\n" );
+ EndMatchmaking();
+ }
+ else
+ {
+ Msg( "Creating a party when we don't want to be in matchmaking, and it has other players in it. Possibly and old party from an old session. Going to MM UI.\n" );
+ bShouldGoToMMUI = true;
+ }
+ break;
+
+ case CSOTFParty_State_IN_MATCH:
+ case CSOTFParty_State_AWAITING_RESERVATION_CONFIRMATION:
+ // We don't have a match, but we're still in a party. Leave matchmaking.
+ // TODO: Once the lobby panels are no longer a nightmare, let this happen.
+ // We dont really want to destroy their party, but it's too much of
+ // a hassle to support now.
+ if ( !BHaveLiveMatch() )
+ {
+ Msg( "Creating a party when we don't want to be in matchmaking, and it has other players in it, and it's live. Leaving matchmaking\n" );
+ EndMatchmaking();
+ }
+ break;
+ default:
+ AssertMsg1( false, "Unhandled party state %d", pParty->GetState() );
+ }
+ }
+
+
+ if ( bShouldGoToMMUI )
+ {
+ if ( IsLadderGroup( pParty->GetMatchGroup() ) )
+ {
+ engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby ladder" );
+ }
+ else if ( IsCasualGroup( pParty->GetMatchGroup() ) )
+ {
+ engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby casual" );
+ }
+ else if ( IsMvMMatchGroup( pParty->GetMatchGroup() ) )
+ {
+ engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby mvm" );
+ }
+
+ BeginMatchmaking( pParty->GetMatchmakingMode() );
+ }
+
+ // Check if a party was instanced on us as a process of accepting an invite
+ if ( m_eAcceptInviteStep == eAcceptInviteStep_JoinParty )
+ {
+ m_eAcceptInviteStep = eAcceptInviteStep_None;
+ Msg( "Party was instanced as a result of accepting invite. Entering matchmaking lobby UI\n" );
+ engine->ClientCmd_Unrestricted( "OpenMatchmakingLobby invited" );
+ BeginMatchmaking( pParty->GetMatchmakingMode() );
+ }
+
+ //m_msgLocalSearchCriteria.set_key( pParty->Obj().search_key() );
+ m_msgLocalSearchCriteria.set_late_join_ok( pParty->Obj().search_late_join_ok() );
+ //m_msgLocalSearchCriteria.set_matchgroups( pParty->Obj().matchgroups() );
+ m_msgLocalSearchCriteria.set_matchmaking_mode( pParty->GetMatchmakingMode() );
+ m_msgLocalSearchCriteria.set_quickplay_game_type( pParty->GetSearchQuickplayGameType() );
+ m_msgLocalSearchCriteria.clear_mvm_missions();
+ #ifdef USE_MVM_TOUR
+ m_msgLocalSearchCriteria.clear_mvm_mannup_tour();
+ #endif // USE_MVM_TOUR
+ if ( pParty->GetMatchmakingMode() == TF_Matchmaking_MVM )
+ {
+ m_msgLocalSearchCriteria.mutable_mvm_missions()->MergeFrom( pParty->Obj().search_mvm_missions() );
+ #ifdef USE_MVM_TOUR
+ if ( pParty->GetSearchPlayForBraggingRights() )
+ m_msgLocalSearchCriteria.set_mvm_mannup_tour( pParty->GetSearchMannUpTourName() );
+ #endif // USE_MVM_TOUR
+ }
+ else if ( pParty->GetMatchmakingMode() == TF_Matchmaking_LADDER )
+ {
+ m_msgLocalSearchCriteria.set_ladder_game_type( pParty->Obj().search_ladder_game_type() );
+ }
+ else if ( pParty->GetMatchmakingMode() == TF_Matchmaking_CASUAL )
+ {
+ m_msgLocalSearchCriteria.mutable_casual_criteria()->CopyFrom( pParty->Obj().search_casual() );
+ }
+ m_bLocalSquadSurplus = false;
+ int iLocalMemberIdx = pParty->GetMemberIndexBySteamID( steamapicontext->SteamUser()->GetSteamID() );
+ if ( iLocalMemberIdx >= 0 )
+ {
+ m_bLocalSquadSurplus = pParty->Obj().members( iLocalMemberIdx ).squad_surplus();
+ }
+ Assert( pParty->Obj().has_wizard_step() );
+ if ( pParty->Obj().has_wizard_step() )
+ {
+ // If entering or leaving the searching state, clear searching stats
+ if ( m_eLocalWizardStep != TF_Matchmaking_WizardStep_SEARCHING || pParty->Obj().wizard_step() != TF_Matchmaking_WizardStep_SEARCHING )
+ {
+ m_msgMatchmakingProgress.Clear();
+ }
+
+ // Get on the same page as the GC. But if we have a pending request to change the current step,
+ // then wait for that to finish. Otherwise the current step could flicker back and forth.
+ if ( s_nNumWizardStepChangesWaitingForReply == 0 )
+ {
+ m_eLocalWizardStep = pParty->Obj().wizard_step();
+ }
+ }
+ }
+ }
+ else
+ {
+ Assert( pParty != NULL );
+ }
+
+ FireGameEventPartyUpdated();
+
+ CheckAssociatePartyAndSteamLobby();
+
+ // Check if we're ready to active the Steam overlay to invite a user
+ CheckReadyToActivateInvite();
+ }
+ else if ( pObject->GetTypeID() == CTFGSLobby::k_nTypeID )
+ {
+ #if GCMATCHMAKING_DEBUG_LEVEL > 0
+ switch ( changeType )
+ {
+ case SOChanged_Create: GCMatchmakingDebugSpew( 1, "Lobby created\n"); break;
+ case SOChanged_Update: GCMatchmakingDebugSpew( 2, "Lobby updated\n"); break;
+ case SOChanged_Destroy: GCMatchmakingDebugSpew( 1, "Lobby destroyed\n"); break;
+ default: AssertMsg1( false, "Bogus change type %d", changeType );
+ }
+ #endif
+
+ CTFGSLobby *pLobby = GetLobby();
+
+ CSteamID currentServer;
+ if ( pLobby && pLobby->GetState() == CSOTFGameServerLobby_State_RUN )
+ {
+ currentServer = pLobby->GetServerID();
+ }
+
+ // We cannot take the Lobby being deleted as a server assignment change, since the GC could crash and fail to
+ // recover lobbies. Or, since lobbies are lazy-loaded from memcache, it may come back up and lazily put us back
+ // into our lobby.
+ //
+ // However, since we have no way to ask the gameserver without being connected to it, we'll treat
+ // lobby-destroyed as canon only if we're not connected to the match. Otherwise, we'll assume the gameserver's
+ // m_bAssignedMatchEnded flag is the authority.
+ //
+ // Thus, this line reads:
+ // - If we got a *new and differing* lobby, propagate it to the m_*Assigned* convars.
+ // - If our lobby *went away*, clear these convars IF:
+ // - We're not connected to a match server
+ // - OR the gameserver concurs that the match is over
+ bool bLobbyChanged = ( currentServer != m_steamIDGCAssignedMatch ) ||
+ ( pLobby && pLobby->GetMatchID() != m_uAssignedMatchID ) ;
+ if ( bLobbyChanged && ( !BConnectedToMatchServer( true ) || pLobby || m_bAssignedMatchEnded ) )
+ {
+ Msg( "Lobby received with a differing steamID. Lobby's: %s CurrentlyAssigned: %s ConnectedToMatchServer: %d HasLobby: %d AssignedMatchEnded: %d\n"
+ , currentServer.Render()
+ , m_steamIDGCAssignedMatch.Render()
+ , BConnectedToMatchServer( true )
+ , pLobby != NULL
+ , m_bAssignedMatchEnded );
+
+ m_bServerAssignmentChanged = true;
+ m_steamIDGCAssignedMatch = currentServer;
+ m_bAssignedMatchEnded = pLobby ? false : m_bAssignedMatchEnded; // If the lobby is still here, we know the match isn't over.
+ m_uAssignedMatchID = pLobby ? pLobby->GetMatchID() : 0;
+ m_eAssignedMatchGroup = pLobby ? pLobby->GetMatchGroup() : k_nMatchGroup_Invalid;
+ // Store match connection history for generic server browser/connection code to reason about which of our
+ // connections was match related.
+ netadr_t connectAdr; // but y is string
+ if ( pLobby && connectAdr.SetFromString( pLobby->GetConnect() ) )
+ {
+ m_vecMatchServerHistory.AddToTail( connectAdr );
+ }
+ }
+
+ //CTFParty *pParty = GetParty();
+
+
+ // Lobby is gone, but we're connected to our match server still.
+ /*if ( pParty && !pLobby && BConnectedToMatchServer( false ) )
+ {
+ const IMatchGroupDescription* pDesc = GetMatchGroupDescription( pParty->GetMatchGroup() );
+
+ if ( pDesc && pDesc->BShouldAutomaticallyRequeueOnMatchEnd() )
+ {
+ SendCreateOrUpdatePartyMsg( TF_Matchmaking_WizardStep_SEARCHING );
+ }
+ }*/
+
+ FireGameEventLobbyUpdated();
+ }
+ // Notifications. Sync/add/delete with what's in our notification drawer
+ else if ( pObject->GetTypeID() == CTFNotification::k_nTypeID )
+ {
+ const CTFNotification* pSONotification = ( const CTFNotification* )( pObject );
+ Msg( "Notification %llu %s: \"%s\"\n",
+ pSONotification->Obj().notification_id(),
+ changeType == SOChanged_Create ? "created" : changeType == SOChanged_Destroy ? "destroyed" : "updated",
+ pSONotification->Obj().notification_string().c_str() );
+
+ // Update existing notification if found
+ bool bFound = false;
+ for ( int i = NotificationQueue_GetNumNotifications() - 1; i >= 0; --i )
+ {
+ CClientNotification *pNotif = dynamic_cast<CClientNotification *>(NotificationQueue_GetByIndex( i ));
+ if ( pNotif && pNotif->NotificationID() == pSONotification->Obj().notification_id() )
+ {
+ Msg( "Notification %llu already displayed, updating\n",
+ pSONotification->Obj().notification_id() );
+ bFound = true;
+ if ( changeType == SOChanged_Destroy )
+ {
+ NotificationQueue_Remove( pNotif );
+ }
+ else
+ {
+ pNotif->Update( pSONotification );
+ }
+ }
+ }
+
+ // Add them to our notifications drawer if not
+ if ( !bFound && changeType != SOChanged_Destroy )
+ {
+ Msg( "New notification %llu arrived: \"%s\"\n",
+ pSONotification->Obj().notification_id(),
+ pSONotification->Obj().notification_string().c_str() );
+ CClientNotification *pClientNotification = new CClientNotification();
+ pClientNotification->Update( pSONotification );
+ NotificationQueue_Add( pClientNotification );
+ }
+ }
+
+// // After here we only care about create or change events
+// if ( changeType == SOChanged_Destroy )
+// {
+// return;
+// }
+//
+// if( pObject->GetTypeID() == CTFGameAccountClient::k_nTypeID )
+// {
+// CTFGameAccountClient *pAccount = (CTFGameAccountClient *)pObject;
+// m_unWinCount = pAccount->GetWins();
+// m_unLossCount = pAccount->GetLosses();
+// }
+// else if ( pObject->GetTypeID() == CTFHeroStandings::k_nTypeID )
+// {
+// CTFHeroStandings *pHeroStandings = (CTFHeroStandings *)pObject;
+// // see if we have an entry for this already
+// int nFoundIndex = -1;
+// for ( int i = 0; i < m_aHeroRecords.Count(); i++ )
+// {
+// if ( m_aHeroRecords[i].m_unHeroID == pHeroStandings->GetHeroID() )
+// {
+// nFoundIndex = i;
+// break;
+// }
+// }
+// if ( nFoundIndex == -1 )
+// {
+// GCHeroRecord_t newHeroStanding;
+// nFoundIndex = m_aHeroRecords.InsertNoSort( newHeroStanding );
+// }
+//
+// m_aHeroRecords[ nFoundIndex ].m_unHeroID = pHeroStandings->GetHeroID();
+// m_aHeroRecords[ nFoundIndex ].m_unWinCount = pHeroStandings->GetWins();
+// m_aHeroRecords[ nFoundIndex ].m_unLossCount = pHeroStandings->GetLosses();
+//
+// m_aHeroRecords.RedoSort();
+// }
+}
+
+//KeyValues *CTFGCClientSystem::GetNewsStory( uint64 unNewsID )
+//{
+// if ( !m_pNewsKeys )
+// return NULL;
+//
+// for ( KeyValues *pItems = m_pNewsKeys->GetFirstSubKey(); pItems; pItems = pItems->GetNextKey() )
+// {
+// if ( !Q_stricmp( pItems->GetName(), "newsitems" ) )
+// {
+// for ( KeyValues *pItem = pItems->GetFirstSubKey(); pItem; pItem = pItem->GetNextKey() )
+// {
+// if ( !Q_stricmp( pItem->GetName(), "newsitem" ) )
+// {
+// for ( KeyValues *pStory = pItem->GetFirstSubKey(); pStory; pStory = pStory->GetNextKey() )
+// {
+// if ( pStory->GetUint64( "gid" ) == unNewsID )
+// {
+// return pStory;
+// }
+// }
+// }
+// }
+// }
+// }
+// return NULL;
+//}
+//
+//KeyValues *CTFGCClientSystem::GetNewsStoryByIndex( int nNewsIndex )
+//{
+// if ( !m_pNewsKeys )
+// return NULL;
+//
+// int nCount = 0;
+// for ( KeyValues *pItems = m_pNewsKeys->GetFirstSubKey(); pItems; pItems = pItems->GetNextKey() )
+// {
+// if ( !Q_stricmp( pItems->GetName(), "newsitems" ) )
+// {
+// for ( KeyValues *pItem = pItems->GetFirstSubKey(); pItem; pItem = pItem->GetNextKey() )
+// {
+// if ( !Q_stricmp( pItem->GetName(), "newsitem" ) )
+// {
+// for ( KeyValues *pStory = pItem->GetFirstSubKey(); pStory; pStory = pStory->GetNextKey() )
+// {
+// if ( nCount >= nNewsIndex )
+// {
+// return pStory;
+// }
+// nCount++;
+// }
+// }
+// }
+// }
+// }
+// return NULL;
+//}
+
+void CTFGCClientSystem::DumpInvites()
+{
+ if ( !m_pSOCache )
+ {
+ Msg( "No SO cache.\n" );
+ return;
+ }
+
+ CSharedObjectTypeCache *pTypeCache = m_pSOCache->FindBaseTypeCache( CTFPartyInvite::k_nTypeID );
+ if ( !pTypeCache )
+ {
+ Msg( "No invites typecache.\n" );
+ return;
+ }
+
+ Msg( "Listing invites in typecache:\n" );
+ for ( uint32 i = 0; i < pTypeCache->GetCount(); i++ )
+ {
+ CTFPartyInvite *pInvite = static_cast<CTFPartyInvite*>( pTypeCache->GetObject( i ) );
+ Msg( "[%u] PartyID = %llu Sender = %s %s\n", i, pInvite->GetGroupID(), pInvite->GetSenderID().Render(), pInvite->GetSenderName() );
+ }
+}
+
+void CTFGCClientSystem::DumpPing()
+{
+ // RTime32 m_rtLastPingFix;
+ // bool m_bPendingPingRefresh;
+ // bool m_bSentInitialPingFix;
+ if ( !m_rtLastPingFix )
+ {
+ TFPingMsg( "No current ping data. Pending refresh: %i, Sent initial fix: %i\n",
+ m_bPendingPingRefresh, m_bSentInitialPingFix );
+ return;
+ }
+ char szLastFix[ k_RTimeRenderBufferSize ] = { 0 };
+ CRTime::Render( m_rtLastPingFix, szLastFix );
+
+ TFPingMsg( "Ping data is current as of %s. Pending refresh: %i, Sent initial fix: %i\n",
+ szLastFix, m_bPendingPingRefresh, m_bSentInitialPingFix );
+ for ( int i = 0; i < m_msgCachedPingUpdate.pingdata_size(); i++ )
+ {
+ Msg( " %5s: %dms, status %i\n",
+ m_msgCachedPingUpdate.pingdata( i ).name().c_str(),
+ m_msgCachedPingUpdate.pingdata( i ).ping(),
+ m_msgCachedPingUpdate.pingdata( i ).ping_status() );
+ }
+}
+
+//CTFGameAccountClient* CTFGCClientSystem::GetGameAccountClient()
+//{
+// if ( !m_pSOCache )
+// return NULL;
+//
+// CSharedObjectTypeCache *pTypeCache = m_pSOCache->GetBaseTypeCache( CTFGameAccountClient::k_nTypeID );
+// if ( pTypeCache && pTypeCache->GetCount() > 0 )
+// {
+// AssertMsg1( pTypeCache->GetCount() == 1, "Client has %d CTFGameAccountClient objects in his cache! He should only have 1.", pTypeCache->GetCount() );
+// CTFGameAccountClient *pObject = dynamic_cast<CTFGameAccountClient*>( pTypeCache->GetObject( pTypeCache->GetCount() - 1 ) );
+// return pObject;
+// }
+// return NULL;
+//}
+//
+//void CTFGCClientSystem::DumpGameAccountClient()
+//{
+// CTFGameAccountClient *pObj = GetGameAccountClient();
+// if ( !pObj )
+// {
+// Msg( "Failed to find CTFGameAccountClient shared object\n" );
+// return;
+// }
+//
+// Msg( "CTFGameAccountClient:\n" );
+// pObj->Dump();
+//}
+
+//CTFBetaParticipation* CTFGCClientSystem::GetBetaParticipation()
+//{
+// if ( !m_pSOCache )
+// return NULL;
+//
+// CSharedObjectTypeCache *pTypeCache = m_pSOCache->GetBaseTypeCache( CTFBetaParticipation::k_nTypeID );
+// if ( pTypeCache && pTypeCache->GetCount() > 0 )
+// {
+// AssertMsg1( pTypeCache->GetCount() == 1, "Client has %d CTFBetaParticipation objects in his cache! He should only have 1.", pTypeCache->GetCount() );
+// CTFBetaParticipation *pObject = dynamic_cast<CTFBetaParticipation*>( pTypeCache->GetObject( pTypeCache->GetCount() - 1 ) );
+// return pObject;
+// }
+// return NULL;
+//}
+//
+//void CTFGCClientSystem::DumpBetaParticipation()
+//{
+// CTFBetaParticipation *pObj = GetBetaParticipation();
+// if ( !pObj )
+// {
+// Msg( "Failed to find beta participation shared object\n" );
+// return;
+// }
+//
+// Msg( "Beta participation:\n" );
+// pObj->Dump();
+//}
+
+void CTFGCClientSystem::DumpParty()
+{
+ CTFParty *pParty = GetParty();
+ if ( !pParty )
+ {
+ Msg( "Failed to find party shared object\n" );
+ return;
+ }
+
+ pParty->SpewDebug();
+}
+
+CTFParty* CTFGCClientSystem::GetParty()
+{
+ if ( !m_pSOCache )
+ return NULL;
+
+ CSharedObjectTypeCache *pTypeCache = m_pSOCache->FindBaseTypeCache( CTFParty::k_nTypeID );
+ if ( pTypeCache && pTypeCache->GetCount() > 0 )
+ {
+ AssertMsg1( pTypeCache->GetCount() == 1, "Client has %d party objects in his cache! He should only have 1.", pTypeCache->GetCount() );
+ return static_cast<CTFParty*>( pTypeCache->GetObject( pTypeCache->GetCount() - 1 ) );
+ }
+ return NULL;
+}
+
+
+void CTFGCClientSystem::CreateNewParty()
+{
+ Assert( GetParty() == NULL );
+ if ( GetParty() )
+ return;
+
+ switch( GetSearchMode() )
+ {
+ case TF_Matchmaking_LADDER:
+ RequestSelectWizardStep( TF_Matchmaking_WizardStep_LADDER );
+ break;
+
+ case TF_Matchmaking_CASUAL:
+ RequestSelectWizardStep( TF_Matchmaking_WizardStep_CASUAL );
+ break;
+
+ default:
+ // Unhandled for now.
+ // TODO: When GetSearchMode() goes away and we just deal with match groups
+ // fixup all these damn switches everywhere
+ Assert( false );
+ break;
+ };
+
+ // Get the party created. This will get our search criteria set. It will
+ // be the criteria of whatever our previous party was. I *think* this is the
+ // most intuitive thing to do, but we can instead use whatever the local guy's
+ // preferred criteria if this feels weird.
+ SendCreateOrUpdatePartyMsg( m_eLocalWizardStep );
+}
+
+CTFGSLobby* CTFGCClientSystem::GetLobby()
+{
+ if ( !m_pSOCache )
+ return NULL;
+
+ CSharedObjectTypeCache *pTypeCache = m_pSOCache->FindBaseTypeCache( CTFGSLobby::k_nTypeID );
+ if ( pTypeCache && pTypeCache->GetCount() > 0 )
+ {
+ AssertMsg1( pTypeCache->GetCount() == 1, "Client has %d lobby objects in his cache! He should only have 1.", pTypeCache->GetCount() );
+ CTFGSLobby *pLobby = dynamic_cast<CTFGSLobby*>( pTypeCache->GetObject( pTypeCache->GetCount() - 1 ) );
+ return pLobby;
+ }
+ return NULL;
+}
+
+bool CTFGCClientSystem::BIsPartyLeader()
+{
+ CTFParty *pParty = GetParty();
+ if ( pParty == NULL )
+ return true;
+ Assert( steamapicontext );
+ Assert( steamapicontext->SteamUser() );
+ if ( pParty->GetLeader() == steamapicontext->SteamUser()->GetSteamID() )
+ return true;
+ return false;
+}
+
+bool CTFGCClientSystem::BHasOutstandingMatchmakingPartyMessage() const
+{
+ return m_pPendingCreateOrUpdatePartyMsg != NULL || s_nNumWizardStepChangesWaitingForReply > 0;
+}
+
+void CTFGCClientSystem::DumpLobby()
+{
+ CTFGSLobby *pLobby = GetLobby();
+ if ( !pLobby )
+ {
+ Msg( "Failed to find lobby shared object\n" );
+ return;
+ }
+
+ pLobby->SpewDebug();
+}
+
+#ifdef _DEBUG
+static ConVar mm_debug_ignore_connect( "mm_debug_ignore_connect", "0", FCVAR_ARCHIVE, "Debug command to discard matchmaking commands to connect to server" );
+#endif
+
+//-----------------------------------------------------------------------------
+#ifdef STAGING_ONLY
+static ConVar tf_competitive_convar_restrictions_disabled( "tf_competitive_convar_restrictions_disabled", "0", FCVAR_NONE, "If set, this will disable competitive convar restrictions." );
+#endif // STAGING_ONLY
+
+bool ForceCompetitiveConvars()
+{
+#ifdef STAGING_ONLY
+ if ( tf_competitive_convar_restrictions_disabled.GetBool() )
+ {
+ return true;
+ }
+#endif // STAGING_ONLY
+
+ bool anyFailures = false;
+
+ Assert( ThreadInMainThread() );
+ for ( ConCommandBase *ccb = g_pCVar->GetCommands(); ccb; ccb = ccb->GetNext() )
+ {
+ if ( ccb->IsCommand() )
+ continue;
+
+ ConVar *pVar = ( ConVar * ) ccb;
+
+ if ( !pVar->IsCompetitiveRestricted() )
+ continue;
+
+ // Hack: This var is created by the dxconfig system, but it doesn't actually exist.
+ // Skip it so we have no vars change when running a clean config.
+ if ( V_stricmp( pVar->GetName(), "r_decal_cullsize" ) == 0 )
+ continue;
+
+ if ( !pVar->SetCompetitiveMode( true ) )
+ anyFailures = true;
+ }
+
+ return !anyFailures;
+}
+
+void CTFGCClientSystem::ConnectToServer( const char *connect )
+{
+ CTFGSLobby *pLobby = GetLobby();
+ Assert( pLobby );
+ if ( !pLobby )
+ return;
+
+ // !TEST! Check convar to stub connection
+ #ifdef _DEBUG
+ if ( mm_debug_ignore_connect.GetBool() )
+ {
+ Warning(" IGNORING request to connect to %s as per mm_debug_ignore_connect\n", connect );
+ return;
+ }
+ #endif
+
+ Msg("Connecting to %s\n", connect );
+ CUtlString connectCmd;
+ connectCmd.Format( "connect %s matchmaking", connect );
+ if ( engine )
+ {
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( m_eAssignedMatchGroup );
+ bool bAllowed = !( pMatchDesc && pMatchDesc->m_params.m_bForceClientSettings ) || ForceCompetitiveConvars();
+ if ( !bAllowed )
+ {
+ // ForceCompetitiveConvars() shouldn't fail
+ Assert( 0 );
+ }
+
+ engine->ClientCmd_Unrestricted( connectCmd.String() );
+ //vgui::surface()->PlaySound( "ui/ui_findmatch_join_01.wav" );
+ }
+ else
+ {
+ Warning( "Failed to reconnect to game server as engine wasn't ready\n" );
+ }
+}
+
+//void CTFGCClientSystem::StartWatchingGame( const CSteamID &gameServerSteamID )
+//{
+// CSteamID steamIDEmpty;
+// StartWatchingGame( gameServerSteamID, steamIDEmpty );
+//}
+//
+//void CTFGCClientSystem::StartWatchingGame( const CSteamID &gameServerSteamID, const CSteamID &watchServerSteamID )
+//{
+// CProtoBufMsg<CMsgWatchGame> msg( k_EMsgGCWatchGame );
+// msg.Body().set_server_steamid( gameServerSteamID.ConvertToUint64() );
+// msg.Body().set_watch_server_steamid( watchServerSteamID.ConvertToUint64() );
+// msg.Body().set_client_version( engine->GetClientVersion() );
+// GCClientSystem()->BSendMessage( msg );
+// Msg( "StartWatchingGame request SteamID: %s, watching SteamID: %s\n", gameServerSteamID.Render(), watchServerSteamID.Render() );
+//}
+//
+//void CTFGCClientSystem::CancelWatchGameRequest()
+//{
+// CProtoBufMsg< CMsgCancelWatchGame > msg( k_EMsgGCCancelWatchGame );
+// GCClientSystem()->BSendMessage( msg );
+//}
+//
+//void CTFGCClientSystem::StartWatchingGameResponse( const CMsgWatchGameResponse &response )
+//{
+// Msg( "Received CMsgWatchGameResponse result %d.\n", response.watch_game_result() );
+//
+// // Tell UI what is going on
+// if ( g_pWatchGameStatus != NULL )
+// {
+// g_pWatchGameStatus->OnWatchGameResult( response.watch_game_result() );
+// }
+//
+// if ( response.watch_game_result() != CMsgWatchGameResponse_WatchGameResult_READY )
+// {
+// return;
+// }
+//
+// if ( tf_auto_create_proxy.GetBool() )
+// {
+// CreateSourceTVProxy( response.source_tv_public_addr(), response.source_tv_private_addr(), response.source_tv_port() );
+// return;
+// }
+//
+// RichPresence()->OnStartedWatchingGame( response.game_server_steamid(), response.watch_server_steamid() );
+//
+// netadr_t serverPublicIPAddr( response.source_tv_public_addr(), response.source_tv_port() );
+// netadr_t serverPrivateIPAddr( response.source_tv_private_addr(), response.source_tv_port() );
+//
+// CUtlString connect;
+//
+// if ( serverPublicIPAddr.GetIP() != serverPrivateIPAddr.GetIP() )
+// {
+// connect.Format( "connect %s %s", serverPublicIPAddr.ToString(), serverPrivateIPAddr.ToString() );
+// }
+// else
+// {
+// connect.Format( "connect %s", serverPublicIPAddr.ToString() );
+// }
+//
+// Msg( "StartWatchingGame: Sending console command: %s\n", connect.String() );
+// engine->ClientCmd_Unrestricted( connect );
+//}
+//
+
+void CTFGCClientSystem::RequestSelectWizardStep( TF_Matchmaking_WizardStep eWizardStep )
+{
+ // We should only be calling this if we're the party leader
+ Assert( BIsPartyLeader() );
+
+ if ( BAllowMatchmakingSearch() )
+ {
+ // Make sure the wizard step makes sense for the search mode we are using
+ switch ( GetSearchMode() )
+ {
+ case TF_Matchmaking_MVM:
+#ifdef USE_MVM_TOUR
+ Assert(
+ eWizardStep == TF_Matchmaking_WizardStep_MVM_PLAY_FOR_BRAGGING_RIGHTS
+ || eWizardStep == TF_Matchmaking_WizardStep_MVM_TOUR_OF_DUTY
+ || eWizardStep == TF_Matchmaking_WizardStep_MVM_CHALLENGE
+ || eWizardStep == TF_Matchmaking_WizardStep_SEARCHING
+ );
+#else // new mm
+ Assert(
+ eWizardStep == TF_Matchmaking_WizardStep_MVM_PLAY_FOR_BRAGGING_RIGHTS
+ || eWizardStep == TF_Matchmaking_WizardStep_MVM_CHALLENGE
+ || eWizardStep == TF_Matchmaking_WizardStep_SEARCHING
+ );
+#endif // USE_MVM_TOUR
+ break;
+ case TF_Matchmaking_LADDER:
+ Assert(
+ eWizardStep == TF_Matchmaking_WizardStep_LADDER
+ || eWizardStep == TF_Matchmaking_WizardStep_SEARCHING );
+ break;
+ case TF_Matchmaking_CASUAL:
+ Assert( eWizardStep == TF_Matchmaking_WizardStep_CASUAL
+ || eWizardStep == TF_Matchmaking_WizardStep_SEARCHING );
+ break;
+ default:
+ AssertMsg1( false, "Invalid matchmaking mode %d", (int)GetSearchMode() );
+ }
+
+ // If we already have a party, or we're asking to start searching, then
+ // ask the GC to set our state.
+ bool bApplyLocally = false;
+ CTFParty* pParty = GetParty();
+ if ( ( pParty != NULL ) || ( eWizardStep == TF_Matchmaking_WizardStep_SEARCHING ) )
+ {
+ SendCreateOrUpdatePartyMsg( eWizardStep );
+ bApplyLocally = ( eWizardStep != TF_Matchmaking_WizardStep_SEARCHING ) || ( pParty && pParty->BOffline() );
+ }
+ else
+ {
+ // We're just setting local options by ourself right now,
+ // nothing exists on the GC. We can apply this change immediately locally.
+ bApplyLocally = true;
+ }
+
+ // Can we apply this change immediately?
+ if ( bApplyLocally )
+ {
+ m_eLocalWizardStep = eWizardStep;
+ FireGameEventPartyUpdated();
+ }
+ }
+}
+
+EMatchmakingUIState CTFGCClientSystem::GetMatchmakingUIState()
+{
+ // User shutdown?
+ if ( !m_bUserWantsToBeInMatchmaking )
+ {
+ return eMatchmakingUIState_Inactive;
+ }
+
+ // Check if we're connected / connecting to any game server
+ switch ( m_eConnectState )
+ {
+ default:
+ AssertMsg1( false, "Unknown connect state %d", m_eConnectState );
+ case eConnectState_Disconnected: // we should have gotten a beginconnect message first, right?
+ break;
+
+ case eConnectState_ConnectingToMatchmade:
+ return eMatchmakingUIState_Connecting;
+
+ case eConnectState_ConnectedToMatchmade:
+ return eMatchmakingUIState_InGame;
+
+ case eConnectState_NonmatchmadeServer:
+ {
+ if ( BAllowMatchMakingInGame() )
+ break;
+
+ // Eh??? How did we connect to this other server without
+ // exiting matchmaking alrady?
+ Assert( !"In eConnectState_NonmatchmadeServer state, but m_bUserWantsToBeInMatchmaking=true" );
+ EndMatchmaking();
+ return eMatchmakingUIState_Inactive;
+ }
+ }
+
+ // We're not connected to a server.
+ // So we should not be in a game right now, unless it's for ranked games
+ if ( !BAllowMatchMakingInGame() )
+ {
+ Assert( !engine->IsInGame() );
+ }
+
+ CTFParty *pParty = GetParty();
+ CTFGSLobby *pLobby = GetLobby();
+
+ if ( pLobby )
+ {
+ switch ( pLobby->GetState() )
+ {
+ case CSOTFGameServerLobby_State_UNKNOWN:
+ default:
+ AssertMsg1( false, "Unexpected lobby state %d", pLobby->GetState() );
+ case CSOTFGameServerLobby_State_RUN:
+ case CSOTFGameServerLobby_State_SERVERSETUP:
+ return eMatchmakingUIState_Connecting;
+// case CSOTFGameServerLobby_State_NOTREADY:
+// case CSOTFGameServerLobby_State_SERVERASSIGN:
+// return eMatchmakingUIState_InQueue;
+ }
+ }
+
+ // Are we in a search party?
+ if ( pParty )
+ {
+ if ( pParty->GetState() == CSOTFParty_State_FINDING_MATCH )
+ {
+ return eMatchmakingUIState_InQueue;
+ }
+ }
+
+ return eMatchmakingUIState_Chat;
+}
+
+void CTFGCClientSystem::AssertMakesSenseToReadSearchCriteria()
+{
+// EMatchmakingUIState eState = GetMatchmakingUIState();
+// switch ( eState )
+// {
+// case eMatchmakingUIState_Chat:
+// case eMatchmakingUIState_InQueue:
+// case eMatchmakingUIState_Connecting:
+// // They might need to update the UI during this state
+// break;
+//
+// case eMatchmakingUIState_Inactive:
+// case eMatchmakingUIState_InGame:
+// default:
+// // Why do you want to know?
+// AssertMsg1( false, "Invalid matchmaking UI state %d", eState );
+// break;
+// }
+}
+
+bool CTFGCClientSystem::BAllowMatchmakingSearch()
+{
+ bool bLeavingIncursPenalty = ( GTFGCClientSystem()->GetAssignedMatchAbandonStatus() == k_EAbandonGameStatus_AbandonWithPenalty );
+ bool bAllowInGame = ( BAllowMatchMakingInGame() && !bLeavingIncursPenalty );
+
+ EMatchmakingUIState eState = GetMatchmakingUIState();
+ switch ( eState )
+ {
+ case eMatchmakingUIState_Chat:
+ case eMatchmakingUIState_InQueue:
+ // They might need to update the UI during this state
+ return true;
+
+ case eMatchmakingUIState_Inactive:
+ case eMatchmakingUIState_Connecting:
+ case eMatchmakingUIState_InGame:
+ if ( bAllowInGame )
+ return true;
+ return false;
+ default:
+ AssertMsg1( false, "Invalid matchmaking UI state %d", eState );
+ // Why do you want to know?
+ return false;
+ }
+}
+
+TF_MatchmakingMode CTFGCClientSystem::GetSearchMode()
+{
+ return m_msgLocalSearchCriteria.matchmaking_mode();
+}
+
+void CTFGCClientSystem::GetSearchChallenges( CMvMMissionSet &challenges )
+{
+ challenges.Clear();
+
+ // O(n^2) goodness...
+ for ( int i = 0 ; i < m_msgLocalSearchCriteria.mvm_missions_size() ; ++i )
+ {
+ int iChallengeIndex = GetItemSchema()->FindMvmMissionByName( m_msgLocalSearchCriteria.mvm_missions( i ).c_str() );
+ if ( iChallengeIndex >= 0 )
+ challenges.SetMissionBySchemaIndex( iChallengeIndex, true );
+ }
+}
+
+void CTFGCClientSystem::SetSearchChallenges( const CMvMMissionSet &challenges )
+{
+ if ( BInternalSetSearchChallenges( challenges ) )
+ FireGameEventPartyUpdated();
+}
+
+bool CTFGCClientSystem::BInternalSetSearchChallenges( const CMvMMissionSet &challenges )
+{
+ if ( !BAllowMatchmakingSearch() )
+ return false;
+
+ if ( !BIsPartyLeader() )
+ {
+ AssertMsg( false, "Not party leader" );
+ return false;
+ }
+
+ // No change?
+ CMvMMissionSet currentChallenges;
+ GetSearchChallenges( currentChallenges );
+ if ( currentChallenges == challenges )
+ {
+ return false;
+ }
+
+ // Apply the change locally
+ m_msgLocalSearchCriteria.clear_mvm_missions();
+ for ( int i = 0 ; i < GetItemSchema()->GetMvmMissions().Count() ; ++i )
+ {
+ if ( challenges.GetMissionBySchemaIndex( i ) )
+ {
+ m_msgLocalSearchCriteria.add_mvm_missions( GetItemSchema()->GetMvmMissionName( i ) );
+ }
+ }
+ if ( m_msgLocalSearchCriteria.mvm_missions_size() == 0 )
+ {
+ m_msgLocalSearchCriteria.add_mvm_missions( "invalid" );
+ }
+
+ // Check if we need to send a message
+ if ( GetParty() != NULL )
+ {
+ CMsgMatchSearchCriteria *pSearchCriteria = GetCreateOrUpdatePartyMsg()->mutable_search_criteria();
+ pSearchCriteria->clear_mvm_missions();
+ pSearchCriteria->mutable_mvm_missions()->MergeFrom( m_msgLocalSearchCriteria.mvm_missions() );
+ }
+
+ // Fire event
+ return true;
+}
+
+bool CTFGCClientSystem::GetSearchJoinLate()
+{
+ CTFParty *pParty = GetParty();
+// if ( pParty == NULL || m_msgLocalSearchCriteria.has_late_join_ok() )
+ if ( pParty == NULL )
+ {
+ return m_msgLocalSearchCriteria.late_join_ok();
+ }
+ return pParty->Obj().search_late_join_ok();
+}
+
+void CTFGCClientSystem::SetSearchJoinLate( bool bJoinLate )
+{
+ if ( !BAllowMatchmakingSearch() )
+ return;
+
+ if ( !BIsPartyLeader() )
+ {
+ AssertMsg( false, "Not party leader" );
+ return;
+ }
+
+ if ( m_msgLocalSearchCriteria.late_join_ok() != bJoinLate )
+ {
+ if ( GetParty() == NULL )
+ {
+ m_msgLocalSearchCriteria.set_late_join_ok( bJoinLate );
+ FireGameEventPartyUpdated();
+ }
+ else
+ {
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->mutable_search_criteria()->set_late_join_ok( bJoinLate );
+ }
+ }
+ //CheckSendAdjustSearchCriteria();
+}
+
+EGameCategory CTFGCClientSystem::GetQuickplayGameType()
+{
+ return (EGameCategory)m_msgLocalSearchCriteria.quickplay_game_type();
+}
+
+void CTFGCClientSystem::SetQuickplayGameType( EGameCategory type )
+{
+ if ( !BAllowMatchmakingSearch() )
+ return;
+
+ if ( !BIsPartyLeader() )
+ {
+ AssertMsg( false, "Not party leader" );
+ return;
+ }
+
+ if ( (EGameCategory)m_msgLocalSearchCriteria.quickplay_game_type() != type )
+ {
+ if ( GetParty() == NULL )
+ {
+ m_msgLocalSearchCriteria.set_quickplay_game_type( type );
+ FireGameEventPartyUpdated();
+ }
+ else
+ {
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->mutable_search_criteria()->set_quickplay_game_type( type );
+ }
+ }
+ //CheckSendAdjustSearchCriteria();
+}
+
+void CTFGCClientSystem::UpdateCustomPingTolerance()
+{
+ bool bEnabled = ConVarRef( "tf_custom_ping_enabled" ).GetBool();
+ uint32 unValue = bEnabled ? (uint32)Max( 0, ConVarRef( "tf_custom_ping" ).GetInt() ) : 0u;
+
+ // Don't queue unnecessary messages
+ if ( m_msgLocalSearchCriteria.custom_ping_tolerance() == unValue )
+ { return; }
+
+ if ( GetParty() && BIsPartyLeader() )
+ {
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ auto *pCriteria = pMsg->mutable_search_criteria();
+ pCriteria->set_custom_ping_tolerance( unValue );
+ }
+
+ m_msgLocalSearchCriteria.set_custom_ping_tolerance( unValue );
+}
+
+void CTFGCClientSystem::SelectCasualMap( uint32 nMapDefIndex, bool bSelected )
+{
+ CCasualCriteriaHelper casualHelper( m_msgLocalSearchCriteria.casual_criteria() );
+ casualHelper.SetMapSelected( nMapDefIndex, bSelected );
+
+ if ( casualHelper.IsValid() || !casualHelper.AnySelected() )
+ {
+ if ( GetParty() != NULL )
+ {
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->mutable_search_criteria()->mutable_casual_criteria()->CopyFrom( casualHelper.GetCasualCriteria() );
+ }
+
+ m_msgLocalSearchCriteria.mutable_casual_criteria()->CopyFrom( casualHelper.GetCasualCriteria() );
+
+ FireGameEventPartyUpdated();
+ }
+}
+
+void CTFGCClientSystem::ClearCasualSearchCriteria()
+{
+ CCasualCriteriaHelper casualHelper( m_msgLocalSearchCriteria.casual_criteria() );
+ if ( casualHelper.AnySelected() )
+ {
+ casualHelper.Clear();
+
+ if ( GetParty() != NULL )
+ {
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->mutable_search_criteria()->mutable_casual_criteria()->CopyFrom( casualHelper.GetCasualCriteria() );
+ }
+
+ m_msgLocalSearchCriteria.mutable_casual_criteria()->CopyFrom( casualHelper.GetCasualCriteria() );
+
+ FireGameEventPartyUpdated();
+ }
+}
+
+bool CTFGCClientSystem::IsCasualMapSelected( uint32 nMapDefIndex ) const
+{
+ CCasualCriteriaHelper casualHelper( m_msgLocalSearchCriteria.casual_criteria() );
+ return casualHelper.IsMapSelected( nMapDefIndex );
+}
+
+bool CTFGCClientSystem::GetLocalPlayerSquadSurplus()
+{
+ return m_bLocalSquadSurplus;
+}
+
+void CTFGCClientSystem::SetLocalPlayerSquadSurplus( bool bSquadSurplus )
+{
+ if ( m_bLocalSquadSurplus != bSquadSurplus )
+ {
+ if ( GetParty() == NULL )
+ {
+ m_bLocalSquadSurplus = bSquadSurplus;
+ FireGameEventPartyUpdated();
+ }
+ else
+ {
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->set_squad_surplus( bSquadSurplus );
+ }
+ }
+ //CheckSendAdjustSearchCriteria();
+}
+
+bool CTFGCClientSystem::BLocalPlayerInventoryHasMvmTicket( void )
+{
+ CPlayerInventory *pLocalInv = TFInventoryManager()->GetLocalInventory();
+ if ( pLocalInv == NULL )
+ return false;
+
+ static CSchemaItemDefHandle pItemDef_MvmTicket( CTFItemSchema::k_rchMvMTicketItemDefName );
+ if ( !pItemDef_MvmTicket )
+ return false;
+
+ for ( int i = 0 ; i < pLocalInv->GetItemCount() ; ++i )
+ {
+ CEconItemView *pItem = pLocalInv->GetItem( i );
+ Assert( pItem );
+ if ( pItem->GetItemDefinition() == pItemDef_MvmTicket )
+ return true;
+ }
+
+ return false;
+}
+
+int CTFGCClientSystem::GetLocalPlayerInventoryMvmTicketCount( void )
+{
+ int nCount = 0;
+
+ CPlayerInventory *pLocalInv = TFInventoryManager()->GetLocalInventory();
+ if ( pLocalInv )
+ {
+ static CSchemaItemDefHandle pItemDef_MvmTicket( CTFItemSchema::k_rchMvMTicketItemDefName );
+ if ( pItemDef_MvmTicket )
+ {
+ for ( int i = 0 ; i < pLocalInv->GetItemCount() ; ++i )
+ {
+ CEconItemView *pItem = pLocalInv->GetItem( i );
+ Assert( pItem );
+ if ( pItem->GetItemDefinition() == pItemDef_MvmTicket )
+ {
+ nCount++;
+ }
+ }
+ }
+ }
+
+ return nCount;
+}
+
+uint32 CTFGCClientSystem::GetLadderType()
+{
+ return m_msgLocalSearchCriteria.has_ladder_game_type() ? m_msgLocalSearchCriteria.ladder_game_type() : k_nMatchGroup_Invalid;
+}
+
+void CTFGCClientSystem::SetLadderType( uint32 nType )
+{
+ if ( !BIsPartyLeader() )
+ {
+ AssertMsg( false, "Not party leader" );
+ return;
+ }
+
+ if ( m_msgLocalSearchCriteria.ladder_game_type() != nType )
+ {
+ if ( !GetParty() )
+ {
+ m_msgLocalSearchCriteria.set_ladder_game_type( nType );
+ FireGameEventPartyUpdated();
+ }
+ else
+ {
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->mutable_search_criteria()->set_ladder_game_type( nType );
+ }
+ }
+}
+
+bool CTFGCClientSystem::BLocalPlayerInventoryHasSquadSurplusVoucher( void )
+{
+ CPlayerInventory *pLocalInv = TFInventoryManager()->GetLocalInventory();
+ if ( pLocalInv == NULL )
+ return false;
+
+ static CSchemaItemDefHandle k_rchMvMSquadSurplusVoucherItemDefName( CTFItemSchema::k_rchMvMSquadSurplusVoucherItemDefName );
+ if ( !k_rchMvMSquadSurplusVoucherItemDefName )
+ return false;
+
+ for ( int i = 0 ; i < pLocalInv->GetItemCount() ; ++i )
+ {
+ CEconItemView *pItem = pLocalInv->GetItem( i );
+ Assert( pItem );
+ if ( pItem->GetItemDefinition() == k_rchMvMSquadSurplusVoucherItemDefName )
+ return true;
+ }
+
+ return false;
+}
+
+int CTFGCClientSystem::GetLocalPlayerInventorySquadSurplusVoucherCount( void )
+{
+ int nCount = 0;
+
+ CPlayerInventory *pLocalInv = TFInventoryManager()->GetLocalInventory();
+ if ( pLocalInv )
+ {
+ static CSchemaItemDefHandle k_rchMvMSquadSurplusVoucherItemDefName( CTFItemSchema::k_rchMvMSquadSurplusVoucherItemDefName );
+ if ( k_rchMvMSquadSurplusVoucherItemDefName )
+ {
+ for ( int i = 0 ; i < pLocalInv->GetItemCount() ; ++i )
+ {
+ CEconItemView *pItem = pLocalInv->GetItem( i );
+ Assert( pItem );
+ if ( pItem->GetItemDefinition() == k_rchMvMSquadSurplusVoucherItemDefName )
+ {
+ nCount++;
+ }
+ }
+ }
+ }
+
+ return nCount;
+}
+
+#ifdef USE_MVM_TOUR
+bool CTFGCClientSystem::BGetLocalPlayerBadgeInfoForTour( int iTourIndex, uint32 *pnBadgeLevel, uint32 *pnCompletedChallenges )
+{
+ Assert( iTourIndex >= 0 );
+ Assert( iTourIndex < GetItemSchema()->GetMvmTours().Count() );
+ Assert( pnBadgeLevel );
+ Assert( pnCompletedChallenges );
+
+ *pnBadgeLevel = 0;
+ *pnCompletedChallenges = 0;
+
+ CPlayerInventory *pLocalInv = TFInventoryManager()->GetLocalInventory();
+ if ( pLocalInv == NULL )
+ return false;
+
+ // We can't search for a badge without knowing which attribute to look for.
+ static CSchemaAttributeDefHandle pAttribDef_MvmChallengeCompleted( CTFItemSchema::k_rchMvMChallengeCompletedMaskAttribName );
+ Assert( pAttribDef_MvmChallengeCompleted );
+ if ( !pAttribDef_MvmChallengeCompleted )
+ return false;
+
+ if ( iTourIndex < 0 || iTourIndex >= GetItemSchema()->GetMvmTours().Count() )
+ {
+ AssertMsg1( false, "Invalid tour index %d", iTourIndex );
+ return false;
+ }
+ const CEconItemDefinition *pBadgeDef = GetItemSchema()->GetMvmTours()[iTourIndex].m_pBadgeItemDef;
+ if ( pBadgeDef == NULL )
+ {
+ Assert( pBadgeDef );
+ return false;
+ }
+
+ for ( int i = 0 ; i < pLocalInv->GetItemCount() ; ++i )
+ {
+ CEconItemView *pBadge = pLocalInv->GetItem( i );
+ Assert( pBadge );
+ if ( pBadge->GetItemDefinition() != pBadgeDef )
+ continue;
+
+ if ( !pBadge->FindAttribute( pAttribDef_MvmChallengeCompleted, pnCompletedChallenges ) )
+ {
+ AssertMsg( false, "Badge missing challenges completed attribute?" );
+ *pnCompletedChallenges = 0;
+ }
+
+ extern uint32 GetItemDescriptionDisplayLevel( const IEconItemInterface *pEconItem );
+ *pnBadgeLevel = GetItemDescriptionDisplayLevel( pBadge );
+ return true;
+ }
+
+ return false;
+}
+
+int CTFGCClientSystem::GetSearchMannUpTourIndex()
+{
+ CTFParty *pParty = GetParty();
+// if ( pParty == NULL || m_msgLocalSearchCriteria.has_late_join_ok() )
+ if ( pParty == NULL )
+ {
+ if ( !m_msgLocalSearchCriteria.play_for_bragging_rights() )
+ {
+ m_msgLocalSearchCriteria.clear_mvm_mannup_tour();
+ return k_iMvmTourIndex_NotMannedUp;
+ }
+ return GetItemSchema()->FindMvmTourByName( m_msgLocalSearchCriteria.mvm_mannup_tour().c_str() );
+ }
+ return pParty->GetSearchMannUpTourIndex();
+}
+
+void CTFGCClientSystem::SetSearchMannUpTourIndex( int idxTour )
+{
+ Assert( GetSearchPlayForBraggingRights() );
+ if ( BInternalSetSearchMannUpTourIndex( idxTour ) )
+ FireGameEventPartyUpdated();
+}
+
+bool CTFGCClientSystem::BInternalSetSearchMannUpTourIndex( int idxTour )
+{
+ if ( !BAllowMatchmakingSearch() )
+ return false;
+
+ if ( !BIsPartyLeader() )
+ {
+ AssertMsg( false, "Not party leader" );
+ return false;
+ }
+
+ // No change?
+ if ( GetSearchMannUpTourIndex() == idxTour )
+ return false;
+
+ const char *pszTourName = "";
+ if ( idxTour >= 0 )
+ {
+ pszTourName = GetItemSchema()->GetMvmTours()[ idxTour ].m_sTourInternalName.Get();
+ }
+ else
+ {
+ Assert( idxTour == k_iMvmTourIndex_Empty );
+ }
+
+ bool bResult = false;
+ if ( GetParty() == NULL )
+ {
+ m_msgLocalSearchCriteria.set_mvm_mannup_tour( pszTourName );
+ bResult = true;
+ }
+ else
+ {
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->mutable_search_criteria()->set_mvm_mannup_tour( pszTourName );
+ }
+
+ // Check if we need to deselect inappropriate challenges
+ if ( idxTour >= 0 )
+ {
+ CMvMMissionSet challenges;
+ GetSearchChallenges( challenges );
+ bool bChanged = false;
+ for ( int i = 0 ; i < GetItemSchema()->GetMvmMissions().Count() ; ++i )
+ {
+ if ( GetItemSchema()->FindMvmMissionInTour( idxTour, i ) < 0 )
+ {
+ if ( challenges.GetMissionBySchemaIndex( i ) )
+ {
+ challenges.SetMissionBySchemaIndex( i, false );
+ bChanged = true;
+ }
+ }
+ }
+ if ( bChanged )
+ {
+ if ( BInternalSetSearchChallenges( challenges ) )
+ bResult = true;
+ }
+ }
+
+ return bResult;
+}
+#endif // USE_MVM_TOUR
+
+bool CTFGCClientSystem::GetSearchPlayForBraggingRights()
+{
+ CTFParty *pParty = GetParty();
+// if ( pParty == NULL || m_msgLocalSearchCriteria.has_late_join_ok() )
+ if ( pParty == NULL )
+ {
+ return m_msgLocalSearchCriteria.play_for_bragging_rights();
+ }
+ return pParty->GetSearchPlayForBraggingRights();
+}
+
+void CTFGCClientSystem::SetSearchPlayForBraggingRights( bool bPlayForBraggingRights )
+{
+ if ( !BAllowMatchmakingSearch() )
+ return;
+
+ if ( !BIsPartyLeader() )
+ {
+ AssertMsg( false, "Not party leader" );
+ return;
+ }
+
+ // Do we need to fire local event?
+ bool bFirePartyUpdated = false;
+
+ // Any change?
+#ifdef USE_MVM_TOUR
+ if ( GetSearchPlayForBraggingRights() != bPlayForBraggingRights )
+ {
+ if ( GetParty() == NULL )
+ {
+ if ( m_msgLocalSearchCriteria.play_for_bragging_rights() != bPlayForBraggingRights )
+ {
+ m_msgLocalSearchCriteria.set_play_for_bragging_rights( bPlayForBraggingRights );
+ bFirePartyUpdated = true;
+ }
+ }
+ else
+ {
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->mutable_search_criteria()->set_play_for_bragging_rights( bPlayForBraggingRights );
+ }
+
+ // Clear tour selection when first entering mann up
+ if ( bPlayForBraggingRights )
+ {
+ if ( BInternalSetSearchMannUpTourIndex( k_iMvmTourIndex_Empty ) )
+ bFirePartyUpdated = true;
+ }
+ }
+
+ // Check if we must deselect the non-Mann-UP challenges
+ if ( !bPlayForBraggingRights )
+ {
+ m_msgLocalSearchCriteria.clear_mvm_mannup_tour();
+ }
+#else // new mm
+ if ( GetSearchPlayForBraggingRights() != bPlayForBraggingRights )
+ {
+ if ( GetParty() == NULL )
+ {
+ if ( m_msgLocalSearchCriteria.play_for_bragging_rights() != bPlayForBraggingRights )
+ {
+ m_msgLocalSearchCriteria.set_play_for_bragging_rights( bPlayForBraggingRights );
+ }
+ }
+ else
+ {
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->mutable_search_criteria()->set_play_for_bragging_rights( bPlayForBraggingRights );
+ }
+
+ bFirePartyUpdated = true;
+ }
+#endif // USE_MVM_TOUR
+
+ if ( bFirePartyUpdated )
+ FireGameEventPartyUpdated();
+}
+
+//void CTFGCClientSystem::CheckSendAdjustSearchCriteria()
+//{
+// if ( !BMakesSenseToWriteSearchCriteria() )
+// {
+// AssertMsg1( false, "Invalid matchmaking UI state %d", GetMatchmakingUIState() );
+// return;
+// }
+//
+// // We only need to do this if we have a party!
+// if ( GetParty() == NULL )
+// {
+// return;
+// }
+//
+// if ( m_msgLocalSearchCriteria.has_map() ||
+// m_msgLocalSearchCriteria.has_challenge() ||
+// m_msgLocalSearchCriteria.has_late_join_ok() ||
+// m_msgLocalSearchCriteria.has_matchgroups() )
+// {
+// }
+//}
+
+void CTFGCClientSystem::SendCreateOrUpdatePartyMsg( TF_Matchmaking_WizardStep eWizardStep )
+{
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->set_wizard_step( eWizardStep );
+
+ // If we don't have a party yet, populate message with our search criteria
+ CTFParty *pParty = GetParty();
+ if ( pParty == NULL )
+ {
+ *pMsg->mutable_search_criteria() = m_msgLocalSearchCriteria;
+ pMsg->set_squad_surplus( m_bLocalSquadSurplus );
+ }
+
+ // Send the steam lobby, if we have one
+ if ( m_steamIDLobby.IsValid() )
+ {
+ if ( pParty == NULL || pParty->GetSteamLobbyID() != m_steamIDLobby )
+ {
+ pMsg->set_steam_lobby_id( m_steamIDLobby.ConvertToUint64() );
+ }
+ }
+
+ pMsg->set_wizard_step( eWizardStep );
+
+ // This is important! Send it now, even if we have a party.
+ m_flSendPartyUpdateMessageTime = 0.f;
+
+// static ConVarRef sv_search_key("sv_search_key");
+// if ( sv_search_key.IsValid() && *sv_search_key.GetString() )
+// {
+// msg.Body().set_key( sv_search_key.GetString() );
+// }
+
+// static ConVarRef dota_matchgroups("dota_matchgroups");
+// if ( dota_matchgroups.IsValid() )
+// {
+// // abort if no matchgroups set
+// if ( dota_matchgroups.GetInt() == 0 )
+// {
+// DOTA_SF_AddErrorMessage( "#DOTA_Matchmaking_NoRegion_Error" );
+// return;
+// }
+//
+// msg.Body().set_matchgroups( dota_matchgroups.GetInt() );
+// }
+}
+
+void CTFGCClientSystem::SendExitMatchmaking( bool bExplicitAbandon )
+{
+ Msg( "Sending request to exit matchmaking system [ abandon = %d ]\n", bExplicitAbandon );
+ CProtoBufMsg<CMsgExitMatchmaking> msg( k_EMsgGCExitMatchmaking );
+ msg.Body().set_explicit_abandon( bExplicitAbandon );
+ msg.Body().set_party_id( GetParty() ? GetParty()->GetGroupID() : 0 );
+ msg.Body().set_lobby_id( GetLobby() ? GetLobby()->GetGroupID() : 0 );
+ GCClientSystem()->BSendMessage( msg );
+
+ // We're done! No more messages!
+ if ( m_pPendingCreateOrUpdatePartyMsg )
+ {
+ delete m_pPendingCreateOrUpdatePartyMsg;
+ m_pPendingCreateOrUpdatePartyMsg = NULL;
+ m_flSendPartyUpdateMessageTime = FLT_MAX;
+ s_nNumWizardStepChangesWaitingForReply = 0;
+ }
+
+ if ( bExplicitAbandon && m_steamIDGCAssignedMatch.IsValid() && !m_bAssignedMatchEnded )
+ {
+ // Consider this match over on our end, since we're not waiting for the lobby to update (the GC may even be gone)
+ GCMatchmakingDebugSpew( 1, "Sending request to exit matchmaking, marking assigned match as ended\n" );
+ m_bAssignedMatchEnded = true;
+ }
+}
+
+void CTFGCClientSystem::SaveCasualSearchCriteriaToDisk()
+{
+ std::string strOut;
+ google::protobuf::TextFormat::PrintToString( m_msgLocalSearchCriteria.casual_criteria(), &strOut );
+ CUtlBuffer bufOut;
+ bufOut.SetBufferType( true, true );
+ bufOut.PutString( strOut.c_str() );
+ g_pFullFileSystem->WriteFile( s_pszCasualCriteriaSaveFileName, NULL, bufOut );
+}
+
+void CTFGCClientSystem::RejoinActiveMatch( void )
+{
+ // Dialog already exists, just quit
+ if ( s_pRejoinLobbyDialog )
+ return;
+
+ if ( enginevgui == NULL || GetClientModeTFNormal()->GameUI() == NULL )
+ return;
+
+ // Check if this player is in Abandon territory, if so warn them
+ EAbandonGameStatus eAbandonStatus = GTFGCClientSystem()->GetAssignedMatchAbandonStatus();
+ const char* pszTitle = "#TF_MM_Rejoin_Title";
+ const char* pszBody = NULL;
+ const char* pszConfirm = "#TF_MM_Rejoin_Confirm";
+ const char* pszCancel = NULL;
+
+ switch ( eAbandonStatus )
+ {
+ case k_EAbandonGameStatus_Safe:
+ pszBody = "#TF_MM_Rejoin_BaseText";
+ pszCancel = "#TF_MM_Rejoin_Leave";
+ break;
+ case k_EAbandonGameStatus_AbandonWithoutPenalty:
+ pszBody = "#TF_MM_Rejoin_AbandonText_NoPenalty";
+ pszCancel = "#TF_MM_Rejoin_Abandon";
+ break;
+ case k_EAbandonGameStatus_AbandonWithPenalty:
+ pszBody = "#TF_MM_Rejoin_AbandonText";
+ pszCancel = "#TF_MM_Rejoin_Abandon";
+ break;
+ }
+
+ s_pRejoinLobbyDialog = vgui::SETUP_PANEL( new CTFRejoinConfirmDialog(
+ pszTitle,
+ pszBody,
+ pszConfirm,
+ pszCancel,
+ &OnRejoinMvMLobbyDialogCallBack,
+ NULL
+ ));
+
+ if ( s_pRejoinLobbyDialog )
+ {
+ s_pRejoinLobbyDialog->Show();
+ // VGUI is being dumb so I need to manually calculate this windows position
+ int sW, sT, dW, dT;
+ vgui::surface()->GetScreenSize( sW, sT );
+ s_pRejoinLobbyDialog->GetSize( dW, dT );
+ s_pRejoinLobbyDialog->SetPos( (sW - dW) / 2, (sT - dT) / 2 );
+ }
+}
+
+void CTFGCClientSystem::BeginMatchmaking( TF_MatchmakingMode mode )
+{
+ Assert( !m_bUserWantsToBeInMatchmaking );
+ m_bUserWantsToBeInMatchmaking = true;
+ m_msgMatchmakingProgress.Clear();
+ m_nPendingAutoJoinPartyID = 0;
+
+ // Disconnect from any server we're already in
+ if ( ( mode != TF_Matchmaking_LADDER ) && ( mode != TF_Matchmaking_CASUAL ) )
+ {
+ engine->ClientCmd_Unrestricted( "disconnect" );
+ }
+
+ TF_Matchmaking_WizardStep eWizardStep = TF_Matchmaking_WizardStep_INVALID;
+ switch ( mode )
+ {
+ case TF_Matchmaking_MVM:
+ eWizardStep = TF_Matchmaking_WizardStep_MVM_PLAY_FOR_BRAGGING_RIGHTS;
+ break;
+
+ case TF_Matchmaking_LADDER:
+ eWizardStep = TF_Matchmaking_WizardStep_LADDER;
+ break;
+
+ case TF_Matchmaking_CASUAL:
+ eWizardStep = TF_Matchmaking_WizardStep_CASUAL;
+ break;
+
+ default:
+ AssertMsg1( false, "Unknown wizard step %d\n", (int)mode );
+ break;
+ }
+
+ // Check if we don't already have a party, then set some default search options
+ CTFParty *pParty = GetParty();
+ if ( pParty == NULL )
+ {
+ m_msgLocalSearchCriteria.set_matchmaking_mode( mode );
+ m_eLocalWizardStep = eWizardStep;
+
+ // Default late join option
+ m_msgLocalSearchCriteria.set_late_join_ok( false );
+
+ // Default Mann Up state based on whether they have a ticket
+ SetSearchPlayForBraggingRights( mode == TF_Matchmaking_MVM && BLocalPlayerInventoryHasMvmTicket() );
+
+ FireGameEventPartyUpdated();
+ }
+ else
+ {
+
+ // Hmmm. we already have a party. We really should already be in the correct mode.
+ if ( pParty->GetMatchmakingMode() != mode && BIsPartyLeader() )
+ {
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->mutable_search_criteria()->set_matchmaking_mode( mode );
+ pMsg->set_wizard_step( eWizardStep );
+ }
+ }
+
+ // Post an event so we'll know that we joined the lobby OK
+ IGameEvent *pEvent = gameeventmanager->CreateEvent( "mm_lobby_member_join" );
+ if ( !pEvent )
+ return;
+ pEvent->SetString( "steamid", CFmtStr("%llu", steamapicontext->SteamUser()->GetSteamID().ConvertToUint64() ) );
+ pEvent->SetInt( "solo", 1 ); // is this always true?
+ gameeventmanager->FireEventClientSide( pEvent );
+
+}
+
+bool CTFGCClientSystem::BAllowMatchMakingInGame( void ) const
+{
+ return !BHaveLiveMatch();
+}
+
+void CTFGCClientSystem::EndMatchmaking( bool bSendAbandonLobby /* = false */)
+{
+ // Set flag, so if GC sends us any further messages, we'll know to ignore them
+ m_bUserWantsToBeInMatchmaking = false;
+ m_bWantToActivateInviteUI = false;
+ m_msgMatchmakingProgress.Clear();
+
+ // If bSendAbandonLobby is false, this will only ask the GC to drop us from our party. If this message races with
+ // us finding a match, the GC will decline.
+ // ( If that happens, the rejoin game in progress dialog will pop up and resolve the race, so you can't accidentally
+ // abandon by canceling queue at the right millisecond )
+ SendExitMatchmaking( bSendAbandonLobby );
+
+ if ( BConnectedToMatchServer( true ) )
+ {
+ // If we were connected to a server we matchmade into, then disconnect
+ switch ( m_eConnectState )
+ {
+ default:
+ AssertMsg1( false, "Unknown connect state %d", m_eConnectState );
+ case eConnectState_NonmatchmadeServer:
+ case eConnectState_Disconnected:
+ break;
+
+ case eConnectState_ConnectingToMatchmade:
+ case eConnectState_ConnectedToMatchmade:
+ Msg( "Disconnecting from matchmade server\n" );
+ engine->ClientCmd_Unrestricted( "disconnect" );
+ break;
+ }
+ }
+}
+
+bool CTFGCClientSystem::BExitMatchmakingAfterDisconnect( void )
+{
+ return BConnectedToMatchServer( true );
+}
+
+void CTFGCClientSystem::LeaveSteamLobby()
+{
+ if ( m_steamIDLobby.IsValid() )
+ {
+ Assert( steamapicontext );
+ Assert( m_steamIDLobby.IsLobby() );
+ if ( steamapicontext )
+ {
+ Msg( "Leaving steam lobby %s\n", m_steamIDLobby.Render() );
+ steamapicontext->SteamMatchmaking()->LeaveLobby( m_steamIDLobby );
+ }
+ m_steamIDLobby = CSteamID();
+ }
+}
+
+int CTFGCClientSystem::CheckSteamLobbyCreated()
+{
+ if ( !m_bUserWantsToBeInMatchmaking )
+ {
+ Assert( m_bUserWantsToBeInMatchmaking ); // why are you calling this?
+ LeaveSteamLobby();
+ return -1;
+ }
+
+ // Already in a lobby?
+ if ( m_steamIDLobby.IsValid() )
+ return 1;
+
+ // Do we have the interfaces we need?
+ if ( steamapicontext == NULL || steamapicontext->SteamMatchmaking() == NULL )
+ return -1;
+
+ // Is a creation request already in progress?
+ if ( m_eCreateLobbyStatus == -1 )
+ return 0;
+
+ Msg( "Creating Steam lobby\n" );
+
+ m_eCreateLobbyStatus = -1;
+ steamapicontext->SteamMatchmaking()->CreateLobby( k_ELobbyTypePrivate, MAX_PLAYERS );
+ return 0;
+}
+
+void CTFGCClientSystem::RequestActivateInvite()
+{
+
+ // What state are we in?
+ switch ( GetMatchmakingUIState() )
+ {
+ case eMatchmakingUIState_Chat:
+ break;
+
+ case eMatchmakingUIState_InQueue:
+ Warning( "Leaving matchmaking queue due to request to active friend invite UI\n" );
+ break;
+
+ default:
+ Warning( "Can only invite friends to party when in the chat state, or the searching state\n" );
+ m_bWantToActivateInviteUI = false;
+ return;
+ }
+
+ // Set flag. We'll try to activate the UI at he earliest opportunity
+ m_bWantToActivateInviteUI = true;
+
+ // Create our party I we don't have one, and also
+ // get us out of the queue, if we're in it.
+ SendCreateOrUpdatePartyMsg( GetWizardStep() );
+
+ // Check if we're ready to activate the UI now
+ CheckReadyToActivateInvite();
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Ask the GC for the latest global casual criteria stats
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::RequestMatchMakerStats() const
+{
+ CProtoBufMsg<CMsgGCRequestMatchMakerStats> msg( k_EMsgGCRequestMatchMakerStats );
+ GCClientSystem()->BSendMessage( msg );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Set our cached global casual criteria stats and figure out the most
+// popular map so we can do some health computations later.
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::SetMatchMakerStats( const CMsgGCMatchMakerStatsResponse newStats )
+{
+ m_MatchMakerStats = newStats;
+
+ // Update m_nMostSearchedMapCount to be the largest in m_CasualCriteriaStats
+ m_nMostSearchedMapCount = 0;
+ for( int iMap=0; iMap < m_MatchMakerStats.map_count_size(); ++iMap )
+ {
+ m_nMostSearchedMapCount = Max( m_nMostSearchedMapCount, m_MatchMakerStats.map_count( iMap ) );
+ }
+
+ // put data_center_population in dict so we don't have to loop over and strcmp everytime we ask for it
+ COMPILE_TIME_ASSERT( ARRAYSIZE( m_dictDataCenterPopulationRatio ) == k_nMatchGroup_Count );
+ Assert( m_MatchMakerStats.matchgroup_data_center_population_size() == k_nMatchGroup_Count );
+ for ( int iMatchGroup=0; iMatchGroup<k_nMatchGroup_Count; ++iMatchGroup )
+ {
+ m_dictDataCenterPopulationRatio[ iMatchGroup ].Purge();
+ const auto& matchgroup_datacenter_population = m_MatchMakerStats.matchgroup_data_center_population( iMatchGroup );
+ for ( int iDataCenter=0; iDataCenter<matchgroup_datacenter_population.data_center_population_size(); ++iDataCenter )
+ {
+ auto dcp = matchgroup_datacenter_population.data_center_population( iDataCenter );
+ m_dictDataCenterPopulationRatio[ iMatchGroup ].Insert( dcp.name().c_str(), dcp.health_ratio() );
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Given a health ratio, get the health data
+//-----------------------------------------------------------------------------
+CTFGCClientSystem::MatchMakerHealthData_t CTFGCClientSystem::GetHealthBracketForRatio( float flRatio ) const
+{
+ CTFGCClientSystem::MatchMakerHealthData_t data;
+ data.m_flRatio = flRatio;
+
+ static const Color colorBad( 128, 128, 128, 60 );
+ static const Color colorOK( 188, 112, 0, 128 );
+ static const Color colorGood( 94, 150, 49, 255 );
+
+ // Walk through our brackets and fine where we fall and setup data accordingly
+ if ( flRatio < 0.3f )
+ {
+ data.m_colorBar = LerpColor( colorBad, colorOK, RemapValClamped( flRatio, 0.2f, 0.3f, 0.f, 1.f ) );
+ data.m_strLocToken = "TF_Casual_QueueEstimation_Bad";
+ }
+ else if ( flRatio < 0.7f )
+ {
+ data.m_colorBar = LerpColor( colorOK, colorGood, RemapValClamped( flRatio, 0.3f, 0.7f, 0.f, 1.f ) );
+ data.m_strLocToken = "TF_Casual_QueueEstimation_OK";
+ }
+ else
+ {
+ data.m_colorBar = colorGood;
+ data.m_strLocToken = "TF_Casual_QueueEstimation_Good";
+ }
+
+ return data;
+}
+
+#ifdef STAGING_ONLY
+ConVar tf_fake_casual_map_stats( "tf_fake_casual_map_stats", "0" );
+#endif
+
+//-----------------------------------------------------------------------------
+// Purpose: Really here just so we can shortcircuit some staging_only debug
+//-----------------------------------------------------------------------------
+inline uint32 GetCountForMap( const CMsgGCMatchMakerStatsResponse msg, int nIndex )
+{
+#ifdef STAGING_ONLY
+ // If we're faking, then fake some stats
+ if ( tf_fake_casual_map_stats.GetBool() )
+ {
+ CUniformRandomStream randomStream;
+ randomStream.SetSeed( nIndex + tf_fake_casual_map_stats.GetInt() );
+ return randomStream.RandomInt( 0, 100000 );
+ }
+#endif
+
+ return msg.map_count( nIndex );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Get the overall health of the current local casual criteria.
+// Currently just takes the best individual map health.
+//-----------------------------------------------------------------------------
+CTFGCClientSystem::MatchMakerHealthData_t CTFGCClientSystem::GetOverallHealthDataForLocalCriteria() const
+{
+ uint32 nMostSearchedCount = m_nMostSearchedMapCount;
+ uint32 nLargestOfSelected = 0;
+ CCasualCriteriaHelper helper( m_msgLocalSearchCriteria.casual_criteria() );
+
+#ifdef STAGING_ONLY
+ if ( tf_fake_casual_map_stats.GetBool() )
+ {
+ nMostSearchedCount = 100000;
+
+ // Force some fake stats if we dont have the baseline message yet
+ if ( m_MatchMakerStats.map_count_size() == 0 )
+ {
+ for( int i=0; i < GetItemSchema()->GetMasterMapsList().Count(); ++i )
+ {
+ if ( helper.IsMapSelected( i ) )
+ {
+ nLargestOfSelected = Max( nLargestOfSelected, GetCountForMap( m_MatchMakerStats, i ) );
+ }
+ }
+ }
+ }
+#endif
+
+ // No data -- we assume bad
+ if ( nMostSearchedCount == 0 )
+ return GetHealthBracketForRatio( 0.f );
+
+ // Go through all the locallty selected maps and find the one with the best health.
+ // Use that to get our estimated overall criteria health.
+ for( int i=0; i < m_MatchMakerStats.map_count_size(); ++i )
+ {
+ if ( helper.IsMapSelected( i ) )
+ {
+ nLargestOfSelected = Max( nLargestOfSelected, GetCountForMap( m_MatchMakerStats, i ) );
+ }
+ }
+
+ return GetHealthBracketForRatio( (float)nLargestOfSelected / (float)nMostSearchedCount );
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Gets the health of a given map
+//-----------------------------------------------------------------------------
+CTFGCClientSystem::MatchMakerHealthData_t CTFGCClientSystem::GetHealthDataForMap( uint32 nMapIndex ) const
+{
+ uint32 nMostSearchedCount = m_nMostSearchedMapCount;
+ uint32 nLargestOfSelected = 0;
+#ifdef STAGING_ONLY
+ if ( tf_fake_casual_map_stats.GetBool() )
+ {
+ nMostSearchedCount = 100000;
+ nLargestOfSelected = GetCountForMap( m_MatchMakerStats, nMapIndex );
+ }
+#endif
+
+ // No data -- we assume bad
+ if ( nMostSearchedCount == 0 )
+ return GetHealthBracketForRatio( 0.f );
+
+ if ( (int)nMapIndex < m_MatchMakerStats.map_count_size() )
+ {
+ nLargestOfSelected = GetCountForMap( m_MatchMakerStats, nMapIndex );
+ }
+
+ return GetHealthBracketForRatio( (float)nLargestOfSelected / (float)nMostSearchedCount );
+}
+
+
+//CON_COMMAND( tf_resend_so_cache, "Resend SO cache" )
+//{
+// // Force a resend of our SO cache.
+// CProtoBufMsg<CMsgForceSOCacheResend> msg( k_EMsgForceSOCacheResend );
+// GCClientSystem()->BSendMessage( msg );
+//}
+//
+//CON_COMMAND( tf_get_news, "Request game news from the GC" )
+//{
+// if ( args.ArgC() < 2 )
+// return;
+//
+// CGCClientJobGetNews *pJob = new CGCClientJobGetNews( GCClientSystem()->GetGCClient(), atoi( args[1] ) );
+// pJob->StartJob( NULL );
+//}
+
+//CON_COMMAND( tf_party_test, "Tests sending a party invite" )
+//{
+// CProtoBufMsg<CMsgInviteToParty> msg( k_EMsgGCInviteToParty );
+// msg.Body().set_steam_id( steamapicontext->SteamUser()->GetSteamID().ConvertToUint64() );
+//// msg.Body().set_client_version( engine->GetClientVersion() );
+// GCClientSystem()->BSendMessage( msg );
+//}
+
+CON_COMMAND( tf_party_debug, "Prints local party objects" )
+{
+ GTFGCClientSystem()->DumpParty();
+}
+
+CON_COMMAND( tf_invite_debug, "Prints local invite objects" )
+{
+ GTFGCClientSystem()->DumpInvites();
+}
+
+CON_COMMAND( tf_lobby_debug, "Prints local lobby objects" )
+{
+ GTFGCClientSystem()->DumpLobby();
+}
+
+//CON_COMMAND( tf_beta_debug, "Prints local dota beta participation object" )
+//{
+// GTFGCClientSystem()->DumpBetaParticipation();
+//}
+
+//CON_COMMAND( tf_game_account_debug, "Prints game account info" )
+//{
+// GTFGCClientSystem()->DumpGameAccountClient();
+//}
+
+//class CGCClientJobNestedTest : public GCSDK::CGCClientJob
+//{
+//public:
+// CGCClientJobNestedTest( GCSDK::CGCClient *pGCClient, int nIndex ) : GCSDK::CGCClientJob( pGCClient ), m_nIndex( nIndex ) { }
+//
+// virtual bool BYieldingRunJob( void *pvStartParam )
+// {
+// Msg( "Nested job %d running!\n", m_nIndex );
+// BYieldingWaitOneFrame();
+// Msg( "Nested job %d done running!\n", m_nIndex );
+// return true;
+// }
+// int m_nIndex;
+//};
+
+////-----------------------------------------------------------------------------
+//// Purpose: Job for being told when the user GC connection is established
+////-----------------------------------------------------------------------------
+//class CGCClientJobClientWelcome : public GCSDK::CGCClientJob
+//{
+//public:
+// CGCClientJobClientWelcome( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
+//
+// virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
+// {
+// GCSDK::CProtoBufMsg<CMsgClientWelcome> msg( pNetPacket );
+//
+// // Validate version
+// int engineClientVersion = engine->GetClientVersion();
+// int gcClientVersion = (int)msg.Body().version();
+//
+// // Version checking is enforced if both sides do not report zero as their version
+// if ( engineClientVersion && gcClientVersion && engineClientVersion != gcClientVersion )
+// {
+// IGameEvent *pEvent = gameeventmanager->CreateEvent( "gc_mismatched_version" );
+// if ( pEvent )
+// {
+// gameeventmanager->FireEventClientSide( pEvent );
+// }
+// }
+//
+// g_bClientReceivedGCWelcome = true;
+//
+// if ( GTFGCClientSystem() && gameeventmanager )
+// {
+// // when client has reconnected to Steam, wipe dashboard caches.
+// if ( Dashboard() )
+// {
+// Dashboard()->ClearDashboardCaches();
+// }
+//
+// Msg( "CGCClientJobUserSessionCreated::BYieldingRunJobFromMsg firing event\n" );
+// IGameEvent *pEvent = gameeventmanager->CreateEvent( "gc_user_session_created" );
+// if ( pEvent )
+// {
+// gameeventmanager->FireEventClientSide( pEvent );
+// }
+// }
+// else
+// {
+// Msg( "CGCClientJobUserSessionCreated::BYieldingRunJobFromMsg not firing event\n" );
+// }
+//
+// if ( DOTAChat() )
+// {
+// if ( !DOTAChat()->HasJoinedStartupChannels() )
+// {
+// DOTAChat()->JoinStartupChannels();
+// }
+// else
+// {
+// DOTAChat()->SetRejoinChannels();
+// }
+// }
+// return true;
+// }
+//};
+//GC_REG_JOB( GCSDK::CGCClient, CGCClientJobClientWelcome, "CGCClientJobClientWelcome", k_EMsgGCClientWelcome, k_EServerTypeGCClient );
+
+////-----------------------------------------------------------------------------
+//// Purpose: Job for being told when the user's GC session is created
+////-----------------------------------------------------------------------------
+//class CGCClientInvitationCreated : public GCSDK::CGCClientJob
+//{
+//public:
+// CGCClientInvitationCreated( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
+//
+// virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
+// {
+// GCSDK::CProtoBufMsg<CMsgInvitationCreated> msg( pNetPacket );
+//
+// CUtlString commandline;
+// commandline.Format( "+invite %llu", msg.Body().group_id() );
+// steamapicontext->SteamFriends()->InviteUserToGame( msg.Body().steam_id(), commandline.String() );
+//
+// return true;
+// }
+//};
+//GC_REG_JOB( GCSDK::CGCClient, CGCClientInvitationCreated, "CGCClientInvitationCreated", k_EMsgGCInvitationCreated, k_EServerTypeGCClient );
+
+class CGCClientMatchmakingProgress : public GCSDK::CGCClientJob
+{
+public:
+ CGCClientMatchmakingProgress( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
+
+ virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
+ {
+ GCSDK::CProtoBufMsg<CMsgMatchmakingProgress> msg( pNetPacket );
+ GTFGCClientSystem()->m_msgMatchmakingProgress = msg.Body();
+ return true;
+ }
+};
+GC_REG_JOB( GCSDK::CGCClient, CGCClientMatchmakingProgress, "CGCClientMatchmakingProgress", k_EMsgGCMatchmakingProgress, k_EServerTypeGCClient );
+
+
+class CGCClientMatchMakerStats : public GCSDK::CGCClientJob
+{
+public:
+ CGCClientMatchMakerStats( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
+
+ virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
+ {
+ GCSDK::CProtoBufMsg<CMsgGCMatchMakerStatsResponse> msg( pNetPacket );
+ GTFGCClientSystem()->SetMatchMakerStats( msg.Body() );
+
+ IGameEvent *event = gameeventmanager->CreateEvent( "matchmaker_stats_updated" );
+ if ( event )
+ {
+ gameeventmanager->FireEventClientSide( event );
+ }
+
+ return true;
+ }
+};
+GC_REG_JOB( GCSDK::CGCClient, CGCClientMatchMakerStats, "CGCClientMatchMakerStats", k_EMsgGCMatchMakerStatsResponse, k_EServerTypeGCClient );
+
+class CGCClientSurveyRequest : public GCSDK::CGCClientJob
+{
+public:
+ CGCClientSurveyRequest( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
+
+ virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
+ {
+ GCSDK::CProtoBufMsg<CMsgGCSurveyRequest> msg( pNetPacket );
+ GTFGCClientSystem()->SetSurveyRequest( msg.Body() );
+
+ return true;
+ }
+};
+GC_REG_JOB( GCSDK::CGCClient, CGCClientSurveyRequest, "CGCClientSurveyRequest", k_EMsgGC_SurveyQuestionRequest, k_EServerTypeGCClient );
+
+
+////-----------------------------------------------------------------------------
+//class CGCClientJobFindSourceTVGamesDebug : public GCSDK::CGCClientJob
+//{
+//public:
+// CGCClientJobFindSourceTVGamesDebug( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient )
+// {
+//
+// }
+//
+// virtual bool BYieldingRunJob( void *pvStartParam )
+// {
+// CProtoBufMsg<CMsgFindSourceTVGames> msg( k_EMsgGCFindSourceTVGames );
+// CProtoBufMsg<CMsgSourceTVGamesResponse> msgResponse( k_EMsgGCSourceTVGamesResponse );
+//
+// static ConVarRef sv_search_key("sv_search_key");
+// if ( sv_search_key.IsValid() && *sv_search_key.GetString() )
+// {
+// msg.Body().set_search_key( sv_search_key.GetString() );
+// }
+//
+// bool bRet = BYldSendMessageAndGetReply( msg, 15, &msgResponse, k_EMsgGCSourceTVGamesResponse );
+// if ( !bRet )
+// {
+// Warning( "CGCClientJobFindSourceTVGamesDebug failed to get reply\n" );
+// return false;
+// }
+//
+// Msg( "CGCClientJobFindSourceTVGamesDebug: %d\n", msgResponse.Body().games_size() );
+// for ( int i = 0; i < msgResponse.Body().games_size(); i++ )
+// {
+// const CSourceTVGame &game = msgResponse.Body().games(i);
+// Msg( " Game %d:\n", i );
+//
+// CUtlString sGoodPlayers;
+// for ( int p = 0; p < game.good_players_size(); p++ )
+// {
+// if ( sGoodPlayers.Length() > 0 )
+// {
+// sGoodPlayers += ", ";
+// }
+// sGoodPlayers += game.good_players(p).name().c_str();
+// }
+//
+// CUtlString sBadPlayers;
+// for ( int p = 0; p < game.bad_players_size(); p++ )
+// {
+// if ( sBadPlayers.Length() > 0 )
+// {
+// sBadPlayers += ", ";
+// }
+// sBadPlayers += game.bad_players(p).name().c_str();
+// }
+//
+// CUtlString sOtherPlayers;
+// for ( int p = 0; p < game.other_players_size(); p++ )
+// {
+// if ( sOtherPlayers.Length() > 0 )
+// {
+// sOtherPlayers += ", ";
+// }
+// sOtherPlayers += game.other_players(p).name().c_str();
+// }
+//
+// CSteamID steamIDServer( game.server_steamid() );
+// Msg( " SteamID: %s\n Good: %s\n Bad: %s\n Other: %s\n", steamIDServer.Render(), sGoodPlayers.Get(), sBadPlayers.Get(), sOtherPlayers.Get() );
+// }
+//
+// return true;
+// }
+//
+//private:
+//};
+
+////-----------------------------------------------------------------------------
+//// Purpose: Receive a broadcast message for notification
+////-----------------------------------------------------------------------------
+//class CGCClientJobDOTABroadcastNotificationClient : public GCSDK::CGCClientJob
+//{
+//public:
+// CGCClientJobDOTABroadcastNotificationClient( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
+//
+// virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
+// {
+// CProtoBufMsg<CMsgDOTABroadcastNotification> msg( pNetPacket );
+//
+// wchar_t wszText[256];
+// if ( g_pVGuiLocalize->ConvertANSIToUnicode( msg.Body().message().c_str(), wszText, sizeof( wszText ) ) <= 0 )
+// return false;
+//
+// // create a console chat message
+// KeyValues *pChatMsg = new KeyValues( "Command::Game::Chat" );
+// KeyValues::AutoDelete autodelete( pChatMsg );
+// pChatMsg->SetString( "run", "all" );
+// pChatMsg->SetUint64( "xuid", 0 );
+// pChatMsg->SetString( "name", "Console" );
+// pChatMsg->SetWString( "chat", wszText );
+//
+// // make each channel receive the message
+// for ( int i = 0; i < DOTAChat()->GetNumChannels(); ++i )
+// {
+// // receive message immediately
+// DOTAChat()->GetChannel(i)->ReceiveMessage( pChatMsg );
+// }
+//
+// return true;
+// }
+//};
+//GC_REG_JOB( GCSDK::CGCClient, CGCClientJobDOTABroadcastNotificationClient, "CGCClientJobDOTABroadcastNotificationClient", k_EMsgGCBroadcastNotification, k_EServerTypeGCClient );
+
+//-----------------------------------------------------------------------------
+//CON_COMMAND( tf_find_source_tv_games, "Request game news from the GC" )
+//{
+// CGCClientJobFindSourceTVGamesDebug *pJob = new CGCClientJobFindSourceTVGamesDebug( GCClientSystem()->GetGCClient() );
+// pJob->StartJob( NULL );
+//}
+
+//CON_COMMAND( tf_set_lobby_details, "Set game/team names" )
+//{
+// if ( args.ArgC() != 6 )
+// {
+// Msg( "Usage: tf_set_lobby_details <game name> <radiant team name> <radiant team logo> <dire team name> <dire team logo>\n" );
+// return;
+// }
+//
+// CTFGSLobby *pLobby = GTFGCClientSystem() ? GTFGCClientSystem()->GetLobby() : NULL;
+// if ( !pLobby )
+// {
+// Msg( "No lobby found.\n" );
+// return;
+// }
+//
+// CProtoBufMsg<CMsgPracticeLobbySetDetails> msg( k_EMsgGCPracticeLobbySetDetails );
+// msg.Body().set_lobby_id( pLobby->GetGroupID() );
+// msg.Body().set_game_name( args[1] );
+// msg.Body().add_team_details(); // radiant
+// msg.Body().add_team_details(); // dire
+// msg.Body().mutable_team_details( DOTA_GC_TEAM_GOOD_GUYS )->set_team_name( args[2] );
+// msg.Body().mutable_team_details( DOTA_GC_TEAM_GOOD_GUYS )->set_team_logo( args[3] );
+// msg.Body().mutable_team_details( DOTA_GC_TEAM_BAD_GUYS )->set_team_name( args[4] );
+// msg.Body().mutable_team_details( DOTA_GC_TEAM_BAD_GUYS )->set_team_logo( args[5] );
+// GCClientSystem()->BSendMessage( msg );
+//}
+//
+//CON_COMMAND( request_today_messages, "Ask the GC for a list of today messages" )
+//{
+// Msg( "Requesting today messages...\n" );
+// CProtoBufMsg<CMsgDOTARequestTodayMessages> msg( k_EMsgGCRequestTodayMessages );
+// GCClientSystem()->BSendMessage( msg );
+//}
+
+
+//class CUtlSortVectorTodayMessageGreater
+//{
+//public:
+// bool Less( const CMsgDOTATodayMessages_TodayMessage& lhs, const CMsgDOTATodayMessages_TodayMessage& rhs, void * )
+// {
+// return lhs.date() > rhs.date();
+// }
+//};
+//
+////-----------------------------------------------------------------------------
+//// Purpose: Receive a list of today messages
+////-----------------------------------------------------------------------------
+//class CGCClientJobDOTATodayMessages : public GCSDK::CGCClientJob
+//{
+//public:
+// CGCClientJobDOTATodayMessages( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
+//
+// virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
+// {
+// CProtoBufMsg<CMsgDOTATodayMessages> msg( pNetPacket );
+//
+// g_pFullFileSystem->CreateDirHierarchy( "resource/flash3/images/today/", "GAME" );
+//
+// CUtlSortVector< CMsgDOTATodayMessages_TodayMessage, CUtlSortVectorTodayMessageGreater > sortedMessageList;
+//
+// int nMessages = msg.Body().messages_size();
+// for ( int i = 0; i < nMessages; i++ )
+// {
+// sortedMessageList.Insert( msg.Body().messages( i ) );
+// }
+//
+// CMsgDOTATodayMessages sortedMessages;
+// for ( int i = 0; i < sortedMessageList.Count(); i++ )
+// {
+// CMsgDOTATodayMessages_TodayMessage *pNewMessage = sortedMessages.add_messages();
+// *pNewMessage = sortedMessageList[i];
+// }
+// if ( tf_debug_today_message_sorting.GetBool() )
+// {
+// Msg ( "\nUNSORTED ***\n = %s\n", sortedMessages.DebugString().c_str() );
+// Msg ( "\nSORTED ***\n = %s\n", msg.Body().DebugString().c_str() );
+// }
+// GTFGCClientSystem()->SetTodayMessages( &sortedMessages );
+//
+// IGameEvent *pEvent = gameeventmanager->CreateEvent( "today_messages_updated" );
+// if ( pEvent )
+// {
+// pEvent->SetInt( "num_messages", nMessages );
+// gameeventmanager->FireEventClientSide( pEvent );
+// }
+//
+// // make sure we have all the today images downloaded
+// for ( int i = 0; i < nMessages; i++ )
+// {
+// const char *pszImageURL = msg.Body().messages( i ).image_url().c_str();
+// if ( pszImageURL && pszImageURL[0] )
+// {
+// const char *pszFilename = V_UnqualifiedFileName( pszImageURL );
+// if ( pszFilename && pszFilename[0] )
+// {
+// char szBuffer[MAX_PATH];
+// szBuffer[0] = '\0';
+// V_strcat( szBuffer, "resource/flash3/images/today/", sizeof( szBuffer ) );
+// V_strcat( szBuffer, pszFilename, sizeof( szBuffer ) );
+// GTFGCClientSystem()->DownloadFile( pszImageURL, szBuffer );
+// }
+// }
+// }
+//
+// return true;
+// }
+//};
+//GC_REG_JOB( GCSDK::CGCClient, CGCClientJobDOTATodayMessages, "CGCClientJobDOTATodayMessages", k_EMsgGCTodayMessages, k_EServerTypeGCClient );
+//
+///*
+//CON_COMMAND( reports_remaining, "Request number of reports remaining this week" )
+//{
+// CProtoBufMsg<CMsgDOTAReportsRemainingRequest> msg( k_EMsgGCReportsRemainingRequest );
+// GCClientSystem()->BSendMessage( msg );
+//
+// Msg( "Report submitted\n" );
+//}
+//*/
+//
+////-----------------------------------------------------------------------------
+//// Purpose: Receive number of reports remaining
+////-----------------------------------------------------------------------------
+//class CGCClientJobDOTAReportsRemaining : public GCSDK::CGCClientJob
+//{
+//public:
+// CGCClientJobDOTAReportsRemaining( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
+//
+// virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
+// {
+// CProtoBufMsg<CMsgDOTAReportsRemainingResponse> msg( pNetPacket );
+// //Msg( "You have %u positive and %u negative reports remaining\n", msg.Body().num_positive_reports_remaining(), msg.Body().num_negative_reports_remaining() );
+// //Msg( "You have %u positive and %u negative reports total\n", msg.Body().num_positive_reports_total(), msg.Body().num_negative_reports_total() );
+//
+// IGameEvent *pEvent = gameeventmanager->CreateEvent( "player_report_counts_updated" );
+// if ( pEvent )
+// {
+// pEvent->SetInt( "positive_remaining", msg.Body().num_positive_reports_remaining() );
+// pEvent->SetInt( "negative_remaining", msg.Body().num_negative_reports_remaining() );
+// pEvent->SetInt( "positive_total", msg.Body().num_positive_reports_total() );
+// pEvent->SetInt( "negative_total", msg.Body().num_negative_reports_total() );
+// gameeventmanager->FireEventClientSide( pEvent );
+// }
+//
+// return true;
+// }
+//};
+//GC_REG_JOB( GCSDK::CGCClient, CGCClientJobDOTAReportsRemaining, "CGCClientJobDOTAReportsRemaining", k_EMsgGCReportsRemainingResponse, k_EServerTypeGCClient );
+//
+////-----------------------------------------------------------------------------
+//// Purpose: Handle response to k_EMsgGCWatchGame (k_EMsgGCWatchGameResponse)
+////-----------------------------------------------------------------------------
+//class CGCClientJobWatchGameResponse : public GCSDK::CGCClientJob
+//{
+//public:
+// CGCClientJobWatchGameResponse( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
+//
+// virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
+// {
+// GCSDK::CProtoBufMsg<CMsgWatchGameResponse> msg( pNetPacket );
+// GTFGCClientSystem()->StartWatchingGameResponse( msg.Body() );
+// return true;
+// }
+//};
+//GC_REG_JOB( GCSDK::CGCClient, CGCClientJobWatchGameResponse, "CGCClientJobWatchGameResponse", k_EMsgGCWatchGameResponse, k_EServerTypeGCClient );
+//
+//
+//#ifdef _WIN32
+//#undef DECLARE_HANDLE
+//#undef INVALID_HANDLE_VALUE
+//#include <windows.h>
+//#include <shellapi.h>
+//#undef GetCurrentDirectory
+//#elif POSIX
+//#include <sys/syscall.h>
+//#include <sys/wait.h>
+//#include <signal.h>
+//#endif
+//
+//void CTFGCClientSystem::CreateSourceTVProxy( uint32 source_tv_public_addr, uint32 source_tv_private_addr, uint32 source_tv_port )
+//{
+// char szExecutablePath[MAX_PATH];
+// if ( g_pFullFileSystem->RelativePathToFullPath( "..", "EXECUTABLE_PATH", szExecutablePath, sizeof( szExecutablePath ) ) )
+// {
+// Q_FixSlashes( szExecutablePath );
+//
+// char szSrcdsProxyBinary[MAX_PATH];
+// Q_snprintf( szSrcdsProxyBinary, sizeof( szSrcdsProxyBinary ), "%s/srcds.exe", szExecutablePath );
+// Q_FixSlashes( szSrcdsProxyBinary );
+//
+// netadr_t serverPublicIPAddr( source_tv_public_addr, source_tv_port );
+// netadr_t serverPrivateIPAddr( source_tv_private_addr, source_tv_port );
+//
+// netadr_t *addr = NULL;
+//
+// if ( serverPublicIPAddr.GetIP() && !serverPublicIPAddr.IsLocalhost() )
+// {
+// addr = &serverPublicIPAddr;
+// }
+// else if ( serverPublicIPAddr.GetIP() != serverPrivateIPAddr.GetIP() && serverPrivateIPAddr.GetIP() && !serverPrivateIPAddr.IsLocalhost() )
+// {
+// addr = &serverPrivateIPAddr;
+// }
+//
+// if (!addr)
+// {
+// Msg("unable to determine address for proxy at public:%s private:%s\n", serverPublicIPAddr.ToString(), serverPrivateIPAddr.ToString() );
+// return;
+// }
+//
+// CUtlString srcds_args;
+//
+// srcds_args.Format( "-console -game dota +tv_relay %s", addr->ToString() );
+//
+//#ifdef _WIN32
+// int nRet = (int) ShellExecute( NULL, "open", szSrcdsProxyBinary, srcds_args.String(), szExecutablePath, 1 /*SW_SHOWNORMAL*/ );
+//
+// if ( nRet > 0 && nRet < 32 )
+// {
+// Warning( "Failed to execute srcds proxy for public:%s private:%s\n", serverPublicIPAddr.ToString(), serverPrivateIPAddr.ToString() );
+// Warning( " szSrcdsProxyBinary = %s\n", szSrcdsProxyBinary );
+// Warning( " szExecutablePath = %s\n", szExecutablePath );
+// Warning( " ShellExecute result = %d\n", nRet );
+// }
+// else
+// {
+// Msg( "Executed srcds proxy: %s args: %s dir:%s\n", szSrcdsProxyBinary, srcds_args.String(), szExecutablePath );
+// }
+//#else
+// /* */
+//#endif
+// }
+//
+//}
+
+
+//-----------------------------------------------------------------------------
+// Purpose: Sends an xp acknowledge via k_EMsgGC_AcknowledgeXP to the GC if
+// we have any outstanding xp sources or a mismatch in our last
+// acknowledged xp and our current one.
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::AcknowledgePendingXPSources( EMatchGroup eMatchGroup ) const
+{
+ if ( !m_pSOCache )
+ return;
+
+ const IMatchGroupDescription* pMatchDesc = GetMatchGroupDescription( eMatchGroup );
+ if ( !pMatchDesc )
+ return;
+
+ bool bHasProgressToAcknowledge = false;
+
+ // Check if we have any xp notifications to acknowledge
+ CSharedObjectTypeCache *pXPTypeCache = m_pSOCache->FindBaseTypeCache( CXPSource::k_nTypeID );
+ if ( pXPTypeCache && pXPTypeCache->GetCount() > 0 )
+ {
+ bHasProgressToAcknowledge = true;
+ }
+
+ if ( !steamapicontext || !steamapicontext->SteamUser() )
+ return;
+
+ // Check if the last acknowledged and the current xp are different.
+ auto nLastAckd = pMatchDesc->m_pProgressionDesc->GetLocalPlayerLastAckdExperience();
+ auto nCurrent = pMatchDesc->m_pProgressionDesc->GetPlayerExperienceBySteamID( steamapicontext->SteamUser()->GetSteamID() );
+
+ if ( nLastAckd != nCurrent )
+ {
+ bHasProgressToAcknowledge = true;
+ }
+
+ if ( bHasProgressToAcknowledge )
+ {
+ // Send a message acknowledging the XP
+ CProtoBufMsg<CMsgAcknowledgeXP> msg( k_EMsgGC_AcknowledgeXP );
+ msg.Body().set_match_group( eMatchGroup );
+ GCClientSystem()->BSendMessage( msg );
+ }
+}
+
+void CTFGCClientSystem::AcknowledgeNotification( uint32 nAccountID, uint64 ulNotificationID ) const
+{
+ if ( !m_pSOCache )
+ return;
+
+ CSharedObjectTypeCache *pTypeCache = m_pSOCache->FindBaseTypeCache( CTFNotification::k_nTypeID );
+ if ( pTypeCache && pTypeCache->GetCount() > 0 )
+ {
+ // Send a message acknowledging our notifications
+ ReliableMsgNotificationAcknowledge *pReliable = new ReliableMsgNotificationAcknowledge;
+
+ auto &msg = pReliable->Msg().Body();
+ msg.set_account_id( nAccountID );
+ msg.set_notification_id( ulNotificationID );
+
+ pReliable->Enqueue();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Hang onto the survey request
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::SetSurveyRequest( const CMsgGCSurveyRequest& msgSurveyRequest )
+{
+ m_msgSurveyRequest = msgSurveyRequest;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Send survey response and clear the stored survey request
+//-----------------------------------------------------------------------------
+void CTFGCClientSystem::SendSurveyResponse( int32 nResponse )
+{
+ Assert( m_msgSurveyRequest.has_question_type() );
+
+ GCSDK::CProtoBufMsg< CMsgGCSurveyResponse > msgSurveyResponse( k_EMsgGC_SurveyQuestionResponse );
+ msgSurveyResponse.Body().set_match_id( m_msgSurveyRequest.match_id() );
+ msgSurveyResponse.Body().set_question_type( m_msgSurveyRequest.question_type() );
+ msgSurveyResponse.Body().set_response( nResponse );
+
+ if ( this->BSendMessage( msgSurveyResponse ) )
+ {
+ ClearSurveyRequest();
+ }
+}
+
+void CTFGCClientSystem::ClearSurveyRequest()
+{
+ m_msgSurveyRequest.Clear();
+}
+
+void CTFGCClientSystem::CheckAssociatePartyAndSteamLobby()
+{
+ if ( !m_bUserWantsToBeInMatchmaking )
+ {
+ LeaveSteamLobby();
+ return;
+ }
+
+ CTFParty *pParty = GetParty();
+ if ( pParty == NULL && m_eAcceptInviteStep == eAcceptInviteStep_None )
+ {
+ // Left party, leave lobby now. We don't do this when we request to leave the party, because the GC may deny to
+ // drop us if we raced with a match starting (See SendExitMatchmaking)
+ if ( m_steamIDLobby.IsValid() )
+ {
+ LeaveSteamLobby();
+ }
+ return;
+ }
+ if ( steamapicontext == NULL || steamapicontext->SteamUser() == NULL || steamapicontext->SteamMatchmaking() == NULL )
+ return; // WAT. How did we create our lobby?
+
+ // Let the leader of the party take charge
+ if ( BIsPartyLeader() )
+ {
+
+ // Make sure we have a Steam lobby. If we don't, initiate creation of one.
+ int r = CheckSteamLobbyCreated();
+ if ( r <= 0 )
+ return; // in progress, or failed
+ Assert( m_steamIDLobby.IsValid() );
+
+ // Do we need to update the party, linking it to the steam lobby?
+ if ( pParty->GetSteamLobbyID() != m_steamIDLobby )
+ {
+ Msg( "Sending GCUpdateParty to associate party %016llX with steam lobby %s\n", pParty->GetGroupID(), m_steamIDLobby.Render() );
+ CMsgCreateOrUpdateParty *pMsg = GetCreateOrUpdatePartyMsg();
+ pMsg->set_steam_lobby_id( m_steamIDLobby.ConvertToUint64() );
+ }
+
+ // Do we need to update the steam lobby, linking it to the party?
+ uint64 nCurrentPartyID = 0;
+ const char *pszData = steamapicontext->SteamMatchmaking()->GetLobbyData( m_steamIDLobby, k_pszSteamLobbyKey_PartyID );
+ sscanf( pszData, "%llX", &nCurrentPartyID );
+ if ( nCurrentPartyID != pParty->GetGroupID() )
+ {
+ Msg( "Setting lobby %s data to associate it with party %016llX\n", m_steamIDLobby.Render(), pParty->GetGroupID() );
+
+ char rchValue[64];
+ V_sprintf_safe( rchValue, "%016llX", pParty->GetGroupID() );
+ steamapicontext->SteamMatchmaking()->SetLobbyData( m_steamIDLobby, k_pszSteamLobbyKey_PartyID, rchValue );
+ }
+ }
+ else
+ {
+ // Check if we're not in the lobby associated with this party.
+ if ( pParty->GetSteamLobbyID() != m_steamIDLobby )
+ {
+
+ // If we're already in a party, get out of it
+ if ( m_steamIDLobby.IsValid() )
+ {
+ Warning( "Leaving steam lobby %s, the party is associated with lobby %s\n", m_steamIDLobby.Render(), pParty->GetSteamLobbyID().Render() );
+ LeaveSteamLobby();
+ }
+
+ // If the party has a lobby, then join it
+ if ( pParty->GetSteamLobbyID().IsValid() )
+ {
+
+ // OK, start joining the lobby.
+ m_steamIDLobby = pParty->GetSteamLobbyID();
+ Msg( "Joining lobby %s\n", m_steamIDLobby.Render() );
+ steamapicontext->SteamMatchmaking()->JoinLobby( m_steamIDLobby );
+ }
+ }
+
+ }
+}
+
+void CTFGCClientSystem::OnSteamLobbyCreated( LobbyCreated_t *pInfo )
+{
+ Assert( !m_steamIDLobby.IsValid() );
+ m_eCreateLobbyStatus = pInfo->m_eResult;
+ if ( m_eCreateLobbyStatus == k_EResultOK )
+ {
+ m_steamIDLobby.SetFromUint64( pInfo->m_ulSteamIDLobby );
+ Msg( "Steam lobby %s created OK\n", m_steamIDLobby.Render() );
+
+ // If they already bailed, then destroy the newly created lobby
+ if ( !m_bUserWantsToBeInMatchmaking )
+ {
+ LeaveSteamLobby();
+ m_eCreateLobbyStatus = k_EResultFail;
+ return;
+ }
+
+ // Check if we should let the GC know what lobby to associate
+ // with our party
+ CheckAssociatePartyAndSteamLobby();
+
+ // Check if now we have everything we need to active the UI
+ CheckReadyToActivateInvite();
+ }
+ else
+ {
+ Warning( "FAILED to create steam lobby, error code %d\n", m_eCreateLobbyStatus );
+ }
+}
+
+void CTFGCClientSystem::OnSteamGameLobbyJoinRequested( GameLobbyJoinRequested_t *pInfo )
+{
+ Msg( "OnSteamGameLobbyJoinRequested(%s)\n", pInfo->m_steamIDLobby.Render() );
+
+ // Transform it into a console command and do it!
+ char rchCommand[256];
+ V_sprintf_safe( rchCommand, "connect_lobby %lld", pInfo->m_steamIDLobby.ConvertToUint64() );
+ engine->ClientCmd_Unrestricted( rchCommand );
+}
+
+void CTFGCClientSystem::AcceptFriendInviteToJoinLobby( const CSteamID &steamIDLobby )
+{
+ Msg( "Disconnecting from current server to accept invite\n" );
+
+ // Whatever matchmaking we're doing right right now, get out of it.
+ EndMatchmaking();
+
+ // Just queue it to be joined at the earliest opportunity
+ Msg( "Ready to join steam lobby at next opportunity\n" );
+ m_steamIDLobbyInviteAccepted = steamIDLobby;
+ m_eAcceptInviteStep = eAcceptInviteStep_ReadyToJoinSteamLobby;
+}
+
+void CTFGCClientSystem::OnSteamLobbyEnter( LobbyEnter_t *pInfo )
+{
+ CSteamID steamIDLobby( pInfo->m_ulSteamIDLobby );
+
+ // Are we expecting this?
+ if ( m_steamIDLobby == steamIDLobby )
+ {
+ return;
+ }
+ if ( m_eAcceptInviteStep != eAcceptInviteStep_JoinSteamLobby )
+ {
+ Assert( m_eAcceptInviteStep == eAcceptInviteStep_None );
+ Assert( BIsPartyLeader() );
+ return;
+ }
+ m_eAcceptInviteStep = eAcceptInviteStep_GetLobbyMetadata;
+ Assert( !m_steamIDLobby.IsValid() );
+
+ // Make sure it succeeded
+ if ( pInfo->m_EChatRoomEnterResponse != k_EChatRoomEnterResponseSuccess )
+ {
+ Warning(" Failed to join Steam lobby with error code %d. Cannot join party\n", pInfo->m_EChatRoomEnterResponse );
+ OnFailedToAcceptInvite();
+ return;
+ }
+
+ // We're in a lobby. Remember that, in case we need to bail for some other
+ // reason.
+ m_steamIDLobby.SetFromUint64( pInfo->m_ulSteamIDLobby );
+ Msg( "OnSteamLobbyEnter(%s)\n", m_steamIDLobby.Render() );
+ Assert( m_steamIDLobby.IsLobby() );
+
+ // Request the lobby data
+ steamapicontext->SteamMatchmaking()->RequestLobbyData( m_steamIDLobby );
+}
+
+void CTFGCClientSystem::OnFailedToAcceptInvite()
+{
+ m_eAcceptInviteStep = eAcceptInviteStep_None;
+ EndMatchmaking();
+
+// !KLUDGE! Well, this is a bit crappy us showing a dialog box in this part of the code...
+
+// IGameEvent *pEvent = gameeventmanager->CreateEvent( "mm_accept_invite_fail" );
+// if ( pEvent )
+// {
+// gameeventmanager->FireEventClientSide( pEvent );
+// }
+
+ ShowMessageBox( "#TF_Matchmaking_AcceptInviteFailTitle", "#TF_Matchmaking_AcceptInviteFailMessage", "#TF_OK" );
+}
+
+void CTFGCClientSystem::AddLocalPlayerSOListener( ISharedObjectListener* pListener, bool bImmediately )
+{
+ if ( bImmediately )
+ {
+ SubscribeToLocalPlayerSOCache( pListener );
+ }
+ else
+ {
+ m_vecDelayedLocalPlayerSOListenersToAdd.AddToTail( pListener );
+ }
+}
+
+void CTFGCClientSystem::RemoveLocalPlayerSOListener( ISharedObjectListener* pListener )
+{
+ // Remove if it was a delayed add
+ auto idx = m_vecDelayedLocalPlayerSOListenersToAdd.Find( pListener );
+ if ( idx != m_vecDelayedLocalPlayerSOListenersToAdd.InvalidIndex() )
+ {
+ m_vecDelayedLocalPlayerSOListenersToAdd.Remove( idx );
+ }
+
+ if ( steamapicontext && steamapicontext->SteamUser() )
+ {
+ CSteamID steamID = steamapicontext->SteamUser()->GetSteamID();
+ GetGCClient()->RemoveSOCacheListener( steamID, pListener );
+ }
+}
+
+
+void CTFGCClientSystem::OnSteamLobbyDataUpdate( LobbyDataUpdate_t *pInfo )
+{
+ CSteamID steamIDLobby( pInfo->m_ulSteamIDLobby );
+ Msg( "OnSteamLobbyDataUpdate(%s)\n", steamIDLobby.Render() );
+
+ // Check if we're in the process of accepting an invite
+ if ( steamIDLobby != m_steamIDLobby )
+ {
+ Assert( steamIDLobby == m_steamIDLobby );
+ return;
+ }
+
+ if ( m_eAcceptInviteStep != eAcceptInviteStep_GetLobbyMetadata )
+ {
+ return;
+ }
+
+ m_eAcceptInviteStep = eAcceptInviteStep_JoinParty;
+
+ // Fetch the party ID
+ uint64 nPartyToJoin = 0;
+ const char *pszData = steamapicontext->SteamMatchmaking()->GetLobbyData( m_steamIDLobby, k_pszSteamLobbyKey_PartyID );
+ sscanf( pszData, "%llX", &nPartyToJoin );
+ if ( nPartyToJoin == 0 )
+ {
+ Warning(" No %s metadata set in steam lobby, cannot join party\n", k_pszSteamLobbyKey_PartyID );
+ OnFailedToAcceptInvite();
+ return;
+ }
+ Msg( "Requesting GC add us to party %016llX\n", nPartyToJoin );
+
+ // Join the party.
+ CProtoBufMsg<CMsgAcceptInvite> msgAcceptInvite( k_EMsgGCAcceptInvite );
+ msgAcceptInvite.Body().set_party_id( nPartyToJoin );
+ msgAcceptInvite.Body().set_steamid_lobby( pInfo->m_ulSteamIDLobby );
+ msgAcceptInvite.Body().set_client_version( engine->GetClientVersion() );
+ GCClientSystem()->BSendMessage( msgAcceptInvite );
+}
+
+class CGCClientAcceptInviteResponse : public GCSDK::CGCClientJob
+{
+public:
+ CGCClientAcceptInviteResponse( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
+
+ virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
+ {
+ GCSDK::CProtoBufMsg<CMsgAcceptInviteResponse> msg( pNetPacket );
+
+ switch ( msg.Body().result_code() )
+ {
+ case k_EResultOK:
+ case k_EResultDuplicateRequest:
+ break;
+
+ case k_EResultInvalidProtocolVer:
+ GTFGCClientSystem()->EndMatchmaking();
+ ShowMessageBox( "#TF_MM_NotCurrentVersionTitle", "#TF_MM_NotCurrentVersionMessage", "#GameUI_OK" );
+ break;
+
+ default:
+ Warning( "Failed to accept invite, result code %d\n", msg.Body().result_code() );
+ GTFGCClientSystem()->OnFailedToAcceptInvite();
+ break;
+ }
+
+ return true;
+ }
+};
+GC_REG_JOB( GCSDK::CGCClient, CGCClientAcceptInviteResponse, "CGCClientAcceptInviteResponse", k_EMsgGCAcceptInviteResponse, k_EServerTypeGCClient );
+
+void CTFGCClientSystem::OnSteamLobbyChatUpdate( LobbyChatUpdate_t *pInfo )
+{
+ if ( m_steamIDLobby.ConvertToUint64() != pInfo->m_ulSteamIDLobby || !m_steamIDLobby.IsValid() )
+ return;
+ CSteamID steamIDWhoChanged( pInfo->m_ulSteamIDUserChanged );
+ if ( !steamIDWhoChanged.IsValid() || steamIDWhoChanged == steamapicontext->SteamUser()->GetSteamID() )
+ return;
+
+ if ( BChatMemberStateChangeRemoved( pInfo->m_rgfChatMemberStateChange ) )
+ {
+ IGameEvent *pEvent = gameeventmanager->CreateEvent( "mm_lobby_member_leave" );
+ if ( !pEvent )
+ return;
+ pEvent->SetString( "steamid", CFmtStr("%llu", steamIDWhoChanged.ConvertToUint64() ) );
+ pEvent->SetInt( "flags", pInfo->m_rgfChatMemberStateChange );
+ gameeventmanager->FireEventClientSide( pEvent );
+ }
+ else if ( pInfo->m_rgfChatMemberStateChange & k_EChatMemberStateChangeEntered )
+ {
+ IGameEvent *pEvent = gameeventmanager->CreateEvent( "mm_lobby_member_join" );
+ if ( !pEvent )
+ return;
+ pEvent->SetString( "steamid", CFmtStr("%llu", steamIDWhoChanged.ConvertToUint64() ) );
+ gameeventmanager->FireEventClientSide( pEvent );
+ }
+}
+
+void PostChatGameEvent( const CSteamID &steamIDPoster, CTFGCClientSystem::ELobbyMsgType eType, const char *pszText )
+{
+ IGameEvent *pEvent = gameeventmanager->CreateEvent( "mm_lobby_chat" );
+ if ( !pEvent )
+ return;
+ pEvent->SetString( "steamid", CFmtStr("%llu", steamIDPoster.ConvertToUint64() ) );
+ pEvent->SetString( "text", pszText );
+ pEvent->SetInt( "type", eType );
+ gameeventmanager->FireEventClientSide( pEvent );
+}
+
+void CTFGCClientSystem::OnSteamLobbyChatMsg( LobbyChatMsg_t *pInfo )
+{
+ if ( pInfo->m_eChatEntryType != k_EChatEntryTypeChatMsg )
+ return;
+
+ CSteamID steamIDPoster;
+ EChatEntryType eEntryType;
+ int nBufferSize = 2048;
+ char *pszText = (char *)calloc( nBufferSize + 4, 1 );
+ int nDataSize = steamapicontext->SteamMatchmaking()->GetLobbyChatEntry( pInfo->m_ulSteamIDLobby, pInfo->m_iChatID, &steamIDPoster, pszText, nBufferSize, &eEntryType );
+ if ( nDataSize > 0 )
+ {
+ PostChatGameEvent( steamIDPoster, ELobbyMsgType(pszText[0]), pszText+1 );
+ }
+
+ free( pszText );
+}
+
+void CTFGCClientSystem::SendSteamLobbyChat( ELobbyMsgType eType, const char *pszText )
+{
+ if ( steamapicontext == NULL )
+ return;
+
+ // If we are going to send it to Steam, let's post the message as a result of the callback
+ // on our own message. That gives them some feedback if their message is not being sent.
+ if ( m_steamIDLobby.IsValid() )
+ {
+ int sz = V_strlen(pszText)+2;
+ char *temp = new char[sz];
+ temp[0] = eType;
+ memcpy( temp+1, pszText, sz-1 );
+ steamapicontext->SteamMatchmaking()->SendLobbyChatMsg( m_steamIDLobby, temp, sz );
+ delete[] temp;
+ }
+ else
+ {
+
+ // Not sending to Steam, just echo locally
+ PostChatGameEvent( steamapicontext->SteamUser()->GetSteamID(), eType, pszText );
+ }
+}
+
+void CTFGCClientSystem::CheckReadyToActivateInvite()
+{
+
+ // Don't bother, if we don't have a pending action to popup the UI
+ if ( !m_bWantToActivateInviteUI )
+ return;
+
+ // What high-level state are we in?
+ switch ( GetMatchmakingUIState() )
+ {
+ case eMatchmakingUIState_Chat:
+ case eMatchmakingUIState_InQueue:
+ break;
+
+ default:
+ Warning( "We missed our opportunity to active the invite UI\n" );
+ m_bWantToActivateInviteUI = false;
+ return;
+ }
+
+ // Make sure we have a Steam lobby. If we don't, initiate creation of one.
+ int r = CheckSteamLobbyCreated();
+ if ( r == 0 )
+ return; // in progress
+
+ // Steam lobby creation finished. No matter what, let's make this the last time we
+ // do this polling work.
+ m_bWantToActivateInviteUI = false;
+
+ // Do we have a steam lobby?
+ if ( r < 0 )
+ {
+ Warning( "Cannot active invite UI. Failed to create Steam Lobby\n" );
+ }
+ else
+ {
+ Assert( m_steamIDLobby.IsValid() );
+
+ // Let's invite some of these muthas
+ Msg( "Activating Steam overlay to process invite to lobby %s\n", m_steamIDLobby.Render() );
+ steamapicontext->SteamFriends()->ActivateGameOverlayInviteDialog( m_steamIDLobby );
+ }
+}
+
+bool CTFGCClientSystem::BConnectedToMatchServer( bool bLiveMatch )
+{
+ // A false bLiveMatch means we don't require the lobby and the match to be currently running.
+ // Meaning, you're connected to the match server, but not necessarily in the match (ie. post-match win podium)
+ if ( bLiveMatch && !BHaveLiveMatch() )
+ {
+ return false;
+ }
+
+ // Are we in the process of connecting, or connected to, our assigned server?
+ if ( m_eConnectState != eConnectState_ConnectedToMatchmade && m_eConnectState != eConnectState_ConnectingToMatchmade )
+ {
+ return false;
+ }
+
+ // If steamIDCurrentServer isn't set yet (it only happens late in the connect) default to assuming it's the right
+ // server if it came from matchmaking. We'll find out otherwise when we finish loading.
+ if ( !bLiveMatch || !m_steamIDCurrentServer.IsValid() || m_steamIDCurrentServer == m_steamIDGCAssignedMatch )
+ {
+ return true;
+ }
+
+ return false;
+}
+
+bool CTFGCClientSystem::BHaveLiveMatch() const
+{
+ // NOTE That we don't check the lobby -- SOChanged() and Update() together decide to set or clear our assigned
+ // match, in a way that considers GC connections being fallible but the gameserver remaining authoritative.
+ // See comments there.
+ return m_steamIDGCAssignedMatch.IsValid() && !m_bAssignedMatchEnded;
+}
+
+EAbandonGameStatus CTFGCClientSystem::GetAssignedMatchAbandonStatus()
+{
+ // Bootcamp is a magical cannot-trust-the-game-server land that never has abandons.
+ //
+ // TODO move this to match description as bNonTrustedMM or something.
+ if ( m_eAssignedMatchGroup == k_nMatchGroup_MvM_Practice )
+ {
+ // Can never abandon from bootcamp
+ return k_EAbandonGameStatus_Safe;
+ }
+
+ if ( BConnectedToMatchServer( true ) )
+ {
+ // Gameserver is authoritative if we're connected to this point.
+ C_TFPlayer *pTFPlayer = C_TFPlayer::GetLocalTFPlayer();
+ CTFGameRules *pTFGameRules = TFGameRules();
+ if ( pTFPlayer && pTFGameRules )
+ {
+ bool bLiveMatch = !pTFGameRules->IsManagedMatchEnded();
+ bool bPenalty = !pTFPlayer->GetMatchSafeToLeave();
+ if ( !bLiveMatch )
+ { return k_EAbandonGameStatus_Safe; }
+ else if ( bPenalty )
+ { return k_EAbandonGameStatus_AbandonWithPenalty; }
+ else
+ { return k_EAbandonGameStatus_AbandonWithoutPenalty; }
+ }
+ }
+
+ // Only fall back to looking at the lobby if we're not in the match, or not loaded enough to look at gamerules. The
+ // gameserver is the authority on MM matches, so this is just getting that data secondhand through the GC.
+
+ // TODO(JohnS): Right now, if we have a match via the GC, assume we're required to return. Ideally we'd pipe the
+ // MatchSafeToLeave flag through the lobby, but right now you'll be told you must return and can find
+ // out otherwise once you connect.
+ if ( BHaveLiveMatch() )
+ { return k_EAbandonGameStatus_AbandonWithPenalty; }
+
+ return k_EAbandonGameStatus_Safe;
+}
+
+EMatchGroup CTFGCClientSystem::GetLiveMatchGroup() const
+{
+ if ( !m_bAssignedMatchEnded )
+ return m_eAssignedMatchGroup;
+
+ return k_nMatchGroup_Invalid;
+}
+
+void CTFGCClientSystem::RejoinLobby( bool bConfirmed )
+{
+ // Ask to GC to Rejoin the game
+ // For now try to immediately join
+
+ // XXX(JohnS): Ideally we need to craft a BeginMatchmaking() call to put them back into the matchmaking system --
+ // right now, after a crash, you will lose your MM state and end up at the main menu after a
+ // crash-rejoin. We cannot just set m_bUserWantsToBeInMatchmaking because this skips most of the other
+ // BeginMatchmaking() setup like wizard step and results in broken UI
+ if ( bConfirmed )
+ {
+ CTFGSLobby *pLobby = GetLobby();
+ if ( pLobby )
+ {
+ ConnectToServer( pLobby->GetConnect() );
+ return;
+ }
+ // Lobby disappeared
+ ShowMessageBox( "#TF_MM_Rejoin_FailedTitle", "#TF_MM_Rejoin_FailedBody", "#GameUI_OK" );
+ Log( "Unable to Rejoin existing Lobby game since the Lobby no longer exists." );
+ }
+
+ // Canceled or no lobby for rejoin
+ EndMatchmaking( BHaveLiveMatch() ? true : false );
+}
+
+bool CTFGCClientSystem::JoinMMMatch()
+{
+ CTFGSLobby *pLobby = GetLobby();
+ if ( pLobby )
+ {
+ ConnectToServer( pLobby->GetConnect() );
+ return true;
+ }
+
+ return false;
+}
+
+void CTFGCClientSystem::LeaveGameAndPrepareToJoinParty( GCSDK::PlayerGroupID_t nPartyID )
+{
+ // Remember that we are expecting to join a party
+ m_nPendingAutoJoinPartyID = nPartyID;
+ m_bUserWantsToBeInMatchmaking = false;
+
+ // Clear any other action that might be in progress
+ m_bWantToActivateInviteUI = false;
+ m_msgMatchmakingProgress.Clear();
+
+ // Leave current server
+ engine->ClientCmd_Unrestricted( "disconnect" );
+}
+
+bool CTFGCClientSystem::BIsPhoneVerified( void )
+{
+ CPlayerInventory *pLocalInv = TFInventoryManager()->GetLocalInventory();
+ if ( !pLocalInv )
+ return false;
+
+ if ( !pLocalInv->GetSOC() )
+ return false;
+
+ CEconGameAccountClient *pGameAccountClient = pLocalInv->GetSOC()->GetSingleton< CEconGameAccountClient >();
+ return ( pGameAccountClient && pGameAccountClient->Obj().has_phone_verified() && pGameAccountClient->Obj().phone_verified() );
+}
+
+bool CTFGCClientSystem::BIsPhoneIdentifying( void )
+{
+ CPlayerInventory *pLocalInv = TFInventoryManager()->GetLocalInventory();
+ if ( !pLocalInv )
+ return false;
+
+ if ( !pLocalInv->GetSOC() )
+ return false;
+
+ CEconGameAccountClient *pGameAccountClient = pLocalInv->GetSOC()->GetSingleton< CEconGameAccountClient >();
+ return ( pGameAccountClient && pGameAccountClient->Obj().has_phone_identifying() && pGameAccountClient->Obj().phone_identifying() );
+}
+
+bool CTFGCClientSystem::BHasCompetitiveAccess( void )
+{
+ CPlayerInventory *pLocalInv = TFInventoryManager()->GetLocalInventory();
+ if ( !pLocalInv )
+ return false;
+
+ if ( !pLocalInv->GetSOC() )
+ return false;
+
+ CEconGameAccountClient *pGameAccountClient = pLocalInv->GetSOC()->GetSingleton< CEconGameAccountClient >();
+ return ( pGameAccountClient && pGameAccountClient->Obj().has_competitive_access() && pGameAccountClient->Obj().competitive_access() );
+}
+
+class CGCClientMvMVictoryInfo : public GCSDK::CGCClientJob
+{
+public:
+ CGCClientMvMVictoryInfo( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
+
+ virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
+ {
+ GCSDK::CProtoBufMsg<CMsgMvMVictoryInfo> msg( pNetPacket );
+
+ CTFHudMannVsMachineStatus *pMannVsMachineStatus = GET_HUDELEMENT( CTFHudMannVsMachineStatus );
+ if ( pMannVsMachineStatus )
+ {
+ pMannVsMachineStatus->MVMVictoryGCResponse( msg.Body() );
+ }
+ else
+ {
+ Warning( "Received CMsgMvMVictoryInfo but CTFHudMannVsMachineStatus does not exist \n" );
+ }
+
+ return true;
+ }
+};
+GC_REG_JOB( GCSDK::CGCClient, CGCClientMvMVictoryInfo, "CGCClientMvMVictoryInfo", k_EMsgGCMvMVictoryInfo, k_EServerTypeGCClient );
+
+
+class CGCLeaveGameAndPrepareToJoinPartyJob : public GCSDK::CGCClientJob
+{
+public:
+ CGCLeaveGameAndPrepareToJoinPartyJob( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
+
+ virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
+ {
+ GCSDK::CProtoBufMsg<CMsgLeaveGameAndPrepareToJoinParty> msg( pNetPacket );
+ GTFGCClientSystem()->LeaveGameAndPrepareToJoinParty( msg.Body().party_id() );
+ return true;
+ }
+};
+GC_REG_JOB( GCSDK::CGCClient, CGCLeaveGameAndPrepareToJoinPartyJob, "CGCClientMvMVictoryInfo", k_EMsgGCLeaveGameAndPrepareToJoinParty, k_EServerTypeGCClient );
+
+
+//-----------------------------------------------------------------------------
+// Purpose: GC Msg handler to receive the periodic world status message
+//-----------------------------------------------------------------------------
+class CGCWorldStatusBroadcast : public GCSDK::CGCClientJob
+{
+public:
+ CGCWorldStatusBroadcast( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {}
+
+ virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
+ {
+ GCSDK::CProtoBufMsg<CMsgTFWorldStatus> msg( pNetPacket );
+
+ GTFGCClientSystem()->SetWorldStatus( msg.Body() );
+
+ DevMsg( "TF world status heartbeat.\n Event: %s\n",
+ msg.Body().beta_stress_test_event_active() ? "Y" : "N" );
+ return true;
+ }
+
+};
+
+GC_REG_JOB( GCSDK::CGCClient, CGCWorldStatusBroadcast, "CGCWorldStatusBroadcast", k_EMsgGC_WorldStatusBroadcast, GCSDK::k_EServerTypeGCClient );
+
+
+#if TF_ANTI_IDLEBOT_VERIFICATION
+
+static void GenerateClientVerificationMD5ForItemList( MD5Context_t& out_md5Context, const CUtlVector<const CEconItemView *>& vecItems, bool bIsVerbose = false )
+{
+ FOR_EACH_VEC( vecItems, i )
+ {
+ const CEconItemView *pEconItemView = vecItems[i];
+ Assert( pEconItemView );
+
+ CEconItemDescription desc;
+ desc.SetHashContext( &out_md5Context );
+ desc.SetVerbose( bIsVerbose );
+ IEconItemDescription::YieldingFillOutEconItemDescription( &desc, GLocalizationProvider(), pEconItemView );
+ }
+}
+
+#ifdef DEBUG
+CON_COMMAND_F( tf_generate_client_verification, "<itemID0> ... <itemIDn>", FCVAR_CLIENTDLL )
+{
+ CUtlVector<const CEconItemView *> vecItems;
+ for ( int i = 1; i < args.ArgC(); i++ )
+ {
+#ifdef POSIX
+ const itemid_t unItemId = static_cast<itemid_t >( atoll( args[i] ) );
+#else
+ const itemid_t unItemId = static_cast<itemid_t >( _atoi64( args[i] ) );
+#endif // LINUX
+ const CEconItemView *pEconItemView = TFInventoryManager()->GetLocalTFInventory()->GetInventoryItemByItemID( unItemId );
+ if ( !pEconItemView )
+ {
+ Msg( "Unable to find item id %llu.\n", unItemId );
+ return;
+ }
+
+ vecItems.AddToTail( pEconItemView );
+ }
+
+ MD5Context_t md5Context;
+ MD5Init( &md5Context );
+ GenerateClientVerificationMD5ForItemList( md5Context, vecItems );
+}
+#endif // DEBUG
+
+class CGCClientHelloResponse : public GCSDK::CGCClientJob
+{
+public:
+ CGCClientHelloResponse( GCSDK::CGCClient *pGCClient ) : GCSDK::CGCClientJob( pGCClient ) { }
+
+ virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
+ {
+ GCSDK::CProtoBufMsg<CGCMsgTFHelloResponse> msg( pNetPacket );
+
+ // Don't send a response if we don't have our inventory from Steam yet. The GC will send down another challenge in
+ // a few seconds.
+ if ( !TFInventoryManager()->GetLocalTFInventory()->RetrievedInventoryFromSteam() )
+ return true;
+
+#ifdef DBEUG
+ {
+ Msg( "Beginning response to client verification challenge:\n" );
+ }
+#endif // DEBUG
+
+ CUtlVector<const CEconItemView *> vecItems;
+ for ( int i = 0; i < msg.Body().version_checksum_size(); i++ )
+ {
+ const itemid_t unItemId = msg.Body().version_checksum(i);
+ const CEconItemView *pEconItemView = TFInventoryManager()->GetLocalTFInventory()->GetInventoryItemByItemID( unItemId );
+ if ( !pEconItemView )
+ {
+ // The GC thought we had this item but we don't agree. It should be impossible to mess with the session on the
+ // GC to add/remove items from the SO cache while we've got the lock we use to build the challenge, but we might
+ // have traded away an item *after* that. For this, we ignore the challenge and hope the GC sends us down another
+ // one with our new current inventory.
+ return true;
+ }
+
+ vecItems.AddToTail( pEconItemView );
+ }
+
+ bool bIsVerbose = false;
+ if ( msg.Body().has_version_verbose() )
+ {
+ bIsVerbose = msg.Body().version_verbose();
+ }
+
+ MD5Context_t md5Context;
+ MD5Init( &md5Context );
+ GenerateClientVerificationMD5ForItemList( md5Context, vecItems, bIsVerbose );
+ GCSDK::CProtoBufMsg<CGCMsgTFSync> msgResponse( k_EMsgGC_ClientVerificationChallengeResponse );
+
+ MD5Context_t md5ContextEx = md5Context;
+ MD5Value_t md5ResultEx;
+ MD5Final( &md5ResultEx.bits[0], &md5ContextEx );
+ msgResponse.Body().set_version_checksum_ex( &md5ResultEx.bits[0], MD5_DIGEST_LENGTH );
+
+ const unsigned int unRandomSeed = msg.Body().version_check();
+
+ int key;
+ if ( (*((bool *)g_pClientPurchaseInterface - 156) ) )
+ {
+ key = kTFDescriptionHash_TextmodeArbitraryKey;
+ }
+ else
+ {
+ int iInstanceCount = engine->GetInstancesRunningCount();
+ key = iInstanceCount > 1 ? kTFDescriptionHash_MultiRunArbitraryKey : kTFDescriptionHash_ValidArbitraryKey;
+ }
+
+ const unsigned int unMungedRandomSeed = unRandomSeed + key;
+ TFDescription_HashDataMunge( &md5Context, unMungedRandomSeed, bIsVerbose, VarArgs( "%d", unMungedRandomSeed ) );
+
+ MD5Value_t md5Result;
+ MD5Final( &md5Result.bits[0], &md5Context );
+
+ msgResponse.Body().set_version_checksum( &md5Result.bits[0], MD5_DIGEST_LENGTH );
+
+ // What language are we running in? We need to send this up so the GC will localize with the same strings we're using.
+ char uilanguage[ 64 ];
+ uilanguage[0] = 0;
+ engine->GetUILanguage( uilanguage, sizeof( uilanguage ) );
+ msgResponse.Body().set_version_check( PchLanguageToELanguage( uilanguage ) );
+
+ // Send back up the challenge identifier to maintain sync.
+ msgResponse.Body().set_version_check_ex( unRandomSeed ^ kTFDescriptionHash_ChallengeXorShenanigans );
+
+ GCClientSystem()->BSendMessage( msgResponse );
+
+ return true;
+ }
+};
+GC_REG_JOB( GCSDK::CGCClient, CGCClientHelloResponse, "CGCClientHelloResponse", k_EMsgGC_ClientVerificationChallenge, k_EServerTypeGCClient );
+
+#endif // TF_ANTI_IDLEBOT_VERIFICATION
+
+#ifdef TF_GC_PING_DEBUG
+// Ping debug commands for spoofin'
+CON_COMMAND( tf_datacenter_ping_override, "Override the ping data we'll report for a specific datacenter." )
+{
+ if ( args.ArgC() != 4 )
+ {
+ ConMsg( "Usage: tf_datacenter_ping_override <datacenter> <ping> <status>\n" );
+ return;
+ }
+
+ const char *pszDC = args[1];
+ uint32 nPing = (uint32)Clamp( V_atoi( args[2] ), 0, INT_MAX );
+ CMsgGCDataCenterPing_Update_Status eStatus = (CMsgGCDataCenterPing_Update_Status)V_atoi( args[3] );
+ GTFGCClientSystem()->SetPingOverride( pszDC, nPing, eStatus );
+
+ ConMsg( "Started overriding datacenter \"%s\" to %ums ping with status %i (enum)\n"
+ "Forcing a ping refresh to submit new data with this override\n",
+ pszDC, nPing, eStatus );
+}
+
+CON_COMMAND( tf_datacenter_clear_ping_override, "Stop overriding ping data." )
+{
+ if ( args.ArgC() != 1 )
+ {
+ ConMsg( "Usage: tf_datacenter_clear_ping_override\n" );
+ return;
+ }
+
+ GTFGCClientSystem()->ClearPingOverrides();
+
+ ConMsg( "Stopped overriding any datacenter ping data\n" );
+}
+#endif