diff options
Diffstat (limited to 'game/client/tf/tf_gc_client.cpp')
| -rw-r--r-- | game/client/tf/tf_gc_client.cpp | 4706 |
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 |