Files
hl2sdk/game/client/tf/tf_gc_client.cpp
T

1554 lines
48 KiB
C++

//========= 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 "econ_item_inventory.h"
#include "tf_item_inventory.h"
#include "tf_hud_mann_vs_machine_status.h"
#include "tf_hud_mainmenuoverride.h"
#include "econ/confirm_dialog.h"
#include "rtime.h"
#include "ienginevgui.h"
#include "clientmode_tf.h"
#include "tf_match_description.h"
#include "tf_progression_description.h"
#include "tf_matchcriteria.h"
#include "tf_xp_source.h"
#include "tf_notification.h"
#include "c_tf_notification.h"
#include "tf_gc_shared.h"
#include "tf_partyclient.h"
#include "tf_matchmaking_dashboard.h"
#include "tf_ladder_data.h"
#include "tf_rating_data.h"
#include "tf_quest_map.h"
#include "tf_quest_map_node.h"
#include "econ_quests.h"
#include "econ_item_description.h"
#include "tf_hud_disconnect_prompt.h"
#include "util_shared.h"
#include <steam/isteamnetworkingutils.h>
#include <steam/isteamnetworkingsockets.h>
#include "filesystem.h"
#include "steam/isteamuser.h"
#include "mini_sha256.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
#ifdef _DEBUG
#define GCMATCHMAKING_DEBUG_LEVEL 4
#else
#define GCMATCHMAKING_DEBUG_LEVEL 1
#endif
ConVar tf_mm_debug_level( "tf_mm_debug_level", "4" );
#define GCMatchmakingDebugSpew( lvl, ...) do { if ( lvl <= GCMATCHMAKING_DEBUG_LEVEL || lvl <= tf_mm_debug_level.GetInt() ) { Msg( __VA_ARGS__); } } while(false)
static ConVar mod_inventory_request_timeout( "mod_inventory_request_timeout", "300", FCVAR_NONE, "Seconds to wait for TF inventory before assuming failure" );
using namespace GCSDK;
static const char* GetWebBaseUrl()
{
switch( GetUniverse() )
{
case k_EUniverseDev:
return "https://teamfortress.local.steam.dev/";
case k_EUniverseBeta:
return "https://beta.teamfortress.com/";
case k_EUniversePublic:
default:
return "https://www.teamfortress.com/";
}
}
static CTFGCClientSystem s_TFGCClientSystem;
CTFGCClientSystem *GTFGCClientSystem() { return &s_TFGCClientSystem; }
static CTFPartyClient s_TFPartyClient;
CTFPartyClient *g_pTFPartyClient = nullptr;
CTFPartyClient* GTFPartyClient() { return g_pTFPartyClient; }
// Dialog Prompt Asking users if they want to rejoin a MvM Game
static CTFRejoinConfirmDialog *s_pRejoinLobbyDialog;
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!" );
}
}
//-----------------------------------------------------------------------------
// Constructor
//-----------------------------------------------------------------------------
CTFGCClientSystem::CTFGCClientSystem( void )
{
// replace base GCClientSystem
SetGCClientSystem( this );
s_pRejoinLobbyDialog = NULL;
}
//-----------------------------------------------------------------------------
// Destructor
//-----------------------------------------------------------------------------
CTFGCClientSystem::~CTFGCClientSystem( void )
{
// Prevent other system from using this pointer after it's destroyed
SetGCClientSystem( NULL );
}
bool CTFGCClientSystem::Init()
{
ListenForGameEvent( "client_disconnect" );
ListenForGameEvent( "client_beginconnect" );
ListenForGameEvent( "server_spawn" );
// Let SDR know that we will likely want access to the relay network, so we're more
// likely to have initial ping data to the clusters ready by the time we ask for it
if ( SteamNetworkingUtils() )
{ SteamNetworkingUtils()->InitRelayNetworkAccess(); }
return true;
}
void CTFGCClientSystem::PostInit()
{
BaseClass::PostInit();
}
//-----------------------------------------------------------------------------
void CTFGCClientSystem::PreInitGC()
{
if ( !m_bRegisteredSharedObjects )
{
// REG_SHARED_OBJECT_SUBCLASS( CTFGameAccountClient );
REG_SHARED_OBJECT_SUBCLASS( CTFParty );
REG_SHARED_OBJECT_SUBCLASS( CTFGSLobby );
// REG_SHARED_OBJECT_SUBCLASS( CTFBetaParticipation );
REG_SHARED_OBJECT_SUBCLASS( CTFPartyInvite );
REG_SHARED_OBJECT_SUBCLASS( CTFLobbyInvite );
REG_SHARED_OBJECT_SUBCLASS( CQuestMapNode );
REG_SHARED_OBJECT_SUBCLASS( CTFRatingData );
REG_SHARED_OBJECT_SUBCLASS( CQuestMapRewardPurchase );
m_bRegisteredSharedObjects = true;
}
}
//-----------------------------------------------------------------------------
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" );
}
}
//-----------------------------------------------------------------------------
void CTFGCClientSystem::LevelShutdownPostEntity()
{
BaseClass::LevelShutdownPostEntity();
}
//-----------------------------------------------------------------------------
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 );
if ( pSOCache )
{
pSOCache->RemoveListener( this );
}
}
else
{
Warning( "CTFGCClientSystem - couldn't add listener because Steam wasn't ready\n" );
}
BaseClass::Shutdown();
}
//-----------------------------------------------------------------------------
void CTFGCClientSystem::FireGameEvent( IGameEvent *event )
{
const char *pEventName = event->GetName();
// Disconnected from gameserver
if ( !Q_stricmp( pEventName, "client_disconnect" ) )
{
m_steamIDCurrentServer.Clear();
m_eConnectState = eConnectState_Disconnected;
// We treat the gameserver as authoritative when we are connected to it -- disconnecting from it may change what
// we believe about still having a live match. (e.g. if we have no pLobby anymore, but were loading into a live
// match so didn't get the m_bAssignedMatchEnded update, we need to reconsider if we think the match is live)
if ( UpdateAssignedLobby() )
{
// Fire this regardless of if our assigned lobby changed, since other SO fields did change.
FireGameEventLobbyUpdated();
}
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
{
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()
{
}
// Backoff api
void CTFGCClientSystem::WebapiInventoryState_t::Backoff()
{
if ( m_nBackoffSec == 0 )
m_nBackoffSec = 20;
else
m_nBackoffSec = (m_nBackoffSec * 12 + 9) / 10; // exponential backoff @ 1.2x factor, round up
m_rtNextRequest = CRTime::RTime32TimeCur() + m_nBackoffSec;
}
void CTFGCClientSystem::WebapiInventoryState_t::RequestSucceeded()
{
m_rtNextRequest = 0;
m_nBackoffSec = 0;
}
bool CTFGCClientSystem::WebapiInventoryState_t::IsBackingOff()
{
return m_rtNextRequest != 0 && CRTime::RTime32TimeCur() <= m_rtNextRequest;
}
void CTFGCClientSystem::WebapiInventoryThink()
{
WebapiInventoryState_t &state = m_WebapiInventory;
// Early out if we are waiting backoff timer
if ( state.IsBackingOff() )
return;
switch ( state.m_eState )
{
case kWebapiInventoryState_Init:
state.m_eState = kWebapiInventoryState_RequestAuthToken;
// fallthrough
case kWebapiInventoryState_RequestAuthToken:
if ( !SteamUser() )
return;
if ( state.m_hSteamAuthTicket != k_HAuthTicketInvalid )
{
SteamUser()->CancelAuthTicket( state.m_hSteamAuthTicket );
state.m_hSteamAuthTicket = k_HAuthTicketInvalid;
}
// Request a ticket from Steam
state.m_hSteamAuthTicket = SteamUser()->GetAuthTicketForWebApi( "tf2sdk" );
if ( state.m_hSteamAuthTicket == k_HAuthTicketInvalid )
{
state.Backoff();
return;
}
state.m_eState = kWebapiInventoryState_WaitingForAuthToken;
break;
case kWebapiInventoryState_WaitingForAuthToken:
// Nothing to do until we get the steam callback; that will advance us to AuthTokenReceived
break;
case kWebapiInventoryState_AuthTokenReceived:
state.m_eState = kWebapiInventoryState_RequestInventory;
// fallthrough
case kWebapiInventoryState_RequestInventory:
{
// need access to http + local steam id
if ( !SteamHTTP() || !SteamUser() )
return;
// Request inventory from teamfortress.com webapi
CFmtStr strUrl( "%swebapi/ISDK/GetInventory/v0001", GetWebBaseUrl() );
state.m_InventoryRequestCompleted.Cancel();
state.m_hInventoryRequest = SteamHTTP()->CreateHTTPRequest( k_EHTTPMethodGET, strUrl.Get() );
if ( state.m_hInventoryRequest == INVALID_HTTPREQUEST_HANDLE )
{
// try again next frame
return;
}
// This mod's appid (NOT tf2's appid)
SteamHTTP()->SetHTTPRequestGetOrPostParameter( state.m_hInventoryRequest, "appid", CNumStr( engine->GetAppID() ) );
// Authentication token
CUtlMemory<char> strHexToken;
int nBufSize = 2 * state.m_bufAuthToken.Count();
strHexToken.EnsureCapacity( nBufSize + 1 );
V_binarytohex( state.m_bufAuthToken.Base(), state.m_bufAuthToken.Count(), strHexToken.Base(), strHexToken.Count() ); // TODO: Fix V_binarytohex; it's O(n^2) due to repeated uses of strncat.
SteamHTTP()->SetHTTPRequestGetOrPostParameter( state.m_hInventoryRequest, "ticket", strHexToken.Base() );
if ( GetUniverse() != k_EUniversePublic )
{
// use beta tf2 appid on non public universes
SteamHTTP()->SetHTTPRequestGetOrPostParameter( state.m_hInventoryRequest, "game_appid", "810" );
}
// If we already have an so cache for this user, include its version so we don't send the whole cache if it's unchanged
CGCClientSharedObjectCache* pExistingSOCache = GetSOCache( SteamUser()->GetSteamID() );
if( pExistingSOCache && pExistingSOCache->BIsSubscribed() )
{
SteamHTTP()->SetHTTPRequestGetOrPostParameter( state.m_hInventoryRequest, "version", CNumStr( pExistingSOCache->GetVersion() ) );
}
// Wait a long time for this, api might be slow / inventories are large
if ( mod_inventory_request_timeout.GetInt() > 0 )
{
SteamHTTP()->SetHTTPRequestNetworkActivityTimeout( state.m_hInventoryRequest, mod_inventory_request_timeout.GetInt() );
}
SteamAPICall_t callResult;
if ( !SteamHTTP()->SendHTTPRequest( state.m_hInventoryRequest, &callResult ) )
{
state.Backoff();
return;
}
state.m_InventoryRequestCompleted.Set( callResult, this, &CTFGCClientSystem::OnWebapiInventoryReceived );
state.m_eState = kWebapiInventoryState_WaitingForInventory;
break;
}
case kWebapiInventoryState_WaitingForInventory:
// nothing to do while we wait for http request. that will advance us to the next state
break;
case kWebapiInventoryState_InventoryReceived:
// We can get returned to this state from any of the 'connected to server' states.
// There are three main ways in which this happens:
// - we stop being connected to a server
// - the user changed their loadout and we need to sign a new auth ticket
// for the server to get items from the gc
// - the server asked steam for their inventory and steam told them that the auth
// ticket was invalid/expired. we will request a new one and the user won't
// get their inventory in-game until that is resolved.
// Cancel any existing server auth ticket until we are connected.
if ( state.m_hServerAuthTicket != k_HAuthTicketInvalid )
{
SteamUser()->CancelAuthTicket( state.m_hServerAuthTicket );
state.m_hServerAuthTicket = k_HAuthTicketInvalid;
}
// Wait to get into game.
if ( !engine->IsConnected() )
return;
state.m_eState = kWebapiInventoryState_BuildServerMessage;
//fallthrough
case kWebapiInventoryState_BuildServerMessage:
{
if ( !engine->IsConnected() )
{
// Disconnected from server or user changed loadout. Cancel auth ticket and reauth when we next connect
state.m_eState = kWebapiInventoryState_InventoryReceived;
return;
}
if ( !SteamUser() )
return;
CGCClientSharedObjectCache* pSOCache = GetSOCache( SteamUser()->GetSteamID() );
if ( !pSOCache )
return;
// Build message to send server listing equipped items
CMsgAuthorizeServerItemRetrieval msgItems;
SDK_SelectItemsToSendToServer( &msgItems, pSOCache );
// Serialize it
int nByteSize = msgItems.ByteSize();
CUtlBuffer bufMsg;
bufMsg.EnsureCapacity( nByteSize );
bufMsg.SeekPut( CUtlBuffer::SEEK_HEAD, nByteSize );
msgItems.SerializeToArray( bufMsg.Base(), nByteSize );
// Base64-encode it for communication across the wire in kv / webapi
// We need this encoded pre-auth-ticket generation.
Base64EncodeIntoUTLMemory( ( const uint8* )bufMsg.Base(), nByteSize, state.m_strMsgItems );
// We now have encoded the latest state of the SO cache into our message -- if that changes, we need to re-build
// our message to the server.
state.m_bLocalChangesApplied = false;
state.m_eState = kWebapiInventoryState_RequestServerAuthToken;
//fallthrough
}
case kWebapiInventoryState_RequestServerAuthToken:
{
if ( !engine->IsConnected() || state.m_bLocalChangesApplied )
{
// Disconnected from server or user changed loadout. Cancel auth ticket and reauth when we next connect
state.m_eState = kWebapiInventoryState_InventoryReceived;
return;
}
if ( !SteamUser() )
return;
// "Sign" the message by including an auth ticket that identifies itself as the hash of the requested items
const char* strMsg = state.m_strMsgItems.Base();
SHA256Digest_t digest;
Sha256_t sha256;
Sha256_Init( &sha256 );
Sha256_Update( &sha256, ( const uint8* )strMsg, V_strlen( strMsg ) );
Sha256_Final( &sha256, digest );
// HACK: Steam limits the length of the 'identity' parameter, so don't use the full hash
// (This weakens our security a bit but the server can't sign tickets so even if they can break the hash
// they can only use it to maybe look at some other items in our inventory.)
constexpr int knHashBytesToUse = 15;
char strDigest[knHashBytesToUse * 2 + 1]; // 2 hex characters per byte, +1 for '\0' terminator
COMPILE_TIME_ASSERT( V_ARRAYSIZE( digest ) == 32 );
V_binarytohex( digest + V_ARRAYSIZE( digest ) - knHashBytesToUse, knHashBytesToUse, strDigest, V_ARRAYSIZE( strDigest ) );
// Request the auth ticket from steam and wait for it to arrive.
state.m_hServerAuthTicket = SteamUser()->GetAuthTicketForWebApi( strDigest );
state.m_eState = kWebapiInventoryState_WaitingForServerAuthToken;
break;
}
case kWebapiInventoryState_WaitingForServerAuthToken:
if ( !engine->IsConnected() || state.m_bLocalChangesApplied )
{
// Disconnected from server or user changed loadout. Cancel auth ticket and reauth when we next connect
state.m_eState = kWebapiInventoryState_InventoryReceived;
return;
}
// Nothing else to do until we get the steam callback; that will advance us to ServerAuthTokenReceived
break;
case kWebapiInventoryState_ServerAuthTokenReceived:
{
if ( !engine->IsConnected() || state.m_bLocalChangesApplied )
{
// Disconnected from server or user changed loadout. Cancel auth ticket and reauth when we next connect
state.m_eState = kWebapiInventoryState_InventoryReceived;
return;
}
// Wait until we are fully loaded to send the user our data
if ( !engine->IsInGame() )
return;
// hex-encode the auth token for sending across the wire
CUtlMemory<char> strHexToken;
int nBufSize = 2 * state.m_bufServerAuthToken.Count();
strHexToken.EnsureCapacity( nBufSize + 1 );
V_binarytohex( state.m_bufServerAuthToken.Base(), state.m_bufServerAuthToken.Count(), strHexToken.Base(), strHexToken.Count() ); // TODO: Fix V_binarytohex; it's O(n^2) due to repeated uses of strncat.
// Build KV and send to server
KeyValues *kv = new KeyValues( "sdk_inventory" );
kv->SetString( "msg", state.m_strMsgItems.Base() );
kv->SetString( "ticket", strHexToken.Base() );
// Add any server-specific fields so it knows what to do with the given inventory items (per-mod loadout may not match the user's real tf2 loadout)
SDK_AddServerInventoryInfo( kv, GetSOCache( SteamUser()->GetSteamID() ) );
// Send to the server
engine->ServerCmdKeyValues( kv );
state.m_eState = kWebapiInventoryState_SentToServer;
break;
}
case kWebapiInventoryState_SentToServer:
if ( !engine->IsConnected() || state.m_bLocalChangesApplied )
{
// Disconnected from server or user changed loadout. Cancel auth ticket and reauth when we next connect
state.m_eState = kWebapiInventoryState_InventoryReceived;
return;
}
if( !engine->IsInGame() )
{
// Probably changing level. Do we need to resend the inventory when we are back into the game?
//state.m_eState = kWebapiInventoryState_ServerAuthTokenReceived;
return;
}
// User is connected, server has their inventory, everything is great from our perspective -- we don't need to do anything.
break;
}
}
void CTFGCClientSystem::ServerRequestEquipment()
{
// Something went wrong on the server side (e.g. steam invalidated our inventory auth ticket)
// Get a new one and try again.
if( m_WebapiInventory.m_eState == kWebapiInventoryState_SentToServer )
{
m_WebapiInventory.m_eState = kWebapiInventoryState_InventoryReceived;
}
else
{
// If we are in any other state, we're already in the process of sending the server a new set of equipment as soon as we can
}
}
void CTFGCClientSystem::LocalInventoryChanged()
{
m_WebapiInventory.m_bLocalChangesApplied = true;
}
void CTFGCClientSystem::OnSteamGetTicketForWebApiResponse( GetTicketForWebApiResponse_t *pInfo )
{
OnWebapiAuthTicketReceived( pInfo );
OnWebapiServerAuthTicketReceived( pInfo );
}
void CTFGCClientSystem::OnWebapiAuthTicketReceived( GetTicketForWebApiResponse_t *pInfo )
{
WebapiInventoryState_t& state = m_WebapiInventory;
if ( state.m_eState != kWebapiInventoryState_WaitingForAuthToken )
return;
if ( pInfo->m_hAuthTicket != state.m_hSteamAuthTicket )
return;
// This is our ticket. Assume failure for now, we'll correct this if we find it worked.
state.Backoff();
state.m_eState = kWebapiInventoryState_RequestAuthToken;
// Check that the request succeeded
if ( pInfo->m_eResult != k_EResultOK )
return;
// Validate the token makes sense
if ( pInfo->m_cubTicket < 0 || pInfo->m_cubTicket > pInfo->k_nCubTicketMaxLength )
return;
// Copy the token
state.m_bufAuthToken.SetCount( pInfo->m_cubTicket );
memcpy( state.m_bufAuthToken.Base(), pInfo->m_rgubTicket, pInfo->m_cubTicket );
// Success
state.RequestSucceeded();
state.m_eState = kWebapiInventoryState_AuthTokenReceived;
}
// This is just copy-paste from above -- might be nice to refactor
void CTFGCClientSystem::OnWebapiServerAuthTicketReceived( GetTicketForWebApiResponse_t* pInfo )
{
WebapiInventoryState_t& state = m_WebapiInventory;
if ( state.m_eState != kWebapiInventoryState_WaitingForServerAuthToken )
return;
if ( pInfo->m_hAuthTicket != state.m_hServerAuthTicket )
return;
// This is our ticket. Assume failure for now, we'll correct this if we find it worked.
state.Backoff();
state.m_eState = kWebapiInventoryState_RequestServerAuthToken;
// Check that the request succeeded
if ( pInfo->m_eResult != k_EResultOK )
return;
// Validate the token makes sense
if ( pInfo->m_cubTicket < 0 || pInfo->m_cubTicket > pInfo->k_nCubTicketMaxLength )
return;
// Copy the token
state.m_bufServerAuthToken.SetCount( pInfo->m_cubTicket );
memcpy( state.m_bufServerAuthToken.Base(), pInfo->m_rgubTicket, pInfo->m_cubTicket );
// Success
state.RequestSucceeded();
state.m_eState = kWebapiInventoryState_ServerAuthTokenReceived;
}
void CTFGCClientSystem::OnWebapiInventoryReceived( HTTPRequestCompleted_t* pInfo, bool bIOFailure )
{
if ( !SteamHTTP() )
return; // probably shutting down, just ignore it
WebapiInventoryState_t& state = m_WebapiInventory;
if ( bIOFailure || !pInfo )
{
Assert( false );
// Failed to communicate with steam
// Free our http request (Can we be sure this is the right one?)
if ( state.m_hInventoryRequest != INVALID_HTTPREQUEST_HANDLE )
{
SteamHTTP()->ReleaseHTTPRequest( state.m_hInventoryRequest );
state.m_hInventoryRequest = INVALID_HTTPREQUEST_HANDLE;
}
return;
}
// Did we lose this request somehow (e.g. reset state while it was in flight)
// Just throw it away
if ( pInfo->m_hRequest != state.m_hInventoryRequest )
{
Assert( false );
SteamHTTP()->ReleaseHTTPRequest( pInfo->m_hRequest );
return;
}
// Assume failure -- we'll correct this if this isn't the case
state.Backoff();
state.m_eState = kWebapiInventoryState_RequestInventory;
// This is our handle -- we'll free it by the end of this.
state.m_hInventoryRequest = INVALID_HTTPREQUEST_HANDLE;
if ( !pInfo->m_bRequestSuccessful || pInfo->m_eStatusCode != k_EHTTPStatusCode200OK )
{
SteamHTTP()->ReleaseHTTPRequest( pInfo->m_hRequest );
return;
}
// Extract the result
uint32 unBytes;
Verify( SteamHTTP()->GetHTTPResponseBodySize( pInfo->m_hRequest, &unBytes ) );
CUtlBuffer bufInventory;
bufInventory.EnsureCapacity( unBytes );
bufInventory.SeekPut( CUtlBuffer::SEEK_HEAD, unBytes );
Verify( SteamHTTP()->GetHTTPResponseBodyData( pInfo->m_hRequest, (uint8*)bufInventory.Base(), unBytes ) );
// We're done with the request now
SteamHTTP()->ReleaseHTTPRequest( pInfo->m_hRequest );
// Parse it to json and extract the data
GCSDK::CWebAPIValues* pValues = GCSDK::CWebAPIValues::ParseJSON( bufInventory );
if ( !pValues )
{
Warning( "Received invalid response to inventory request\n" );
return;
}
int nResult = pValues->GetChildInt32Value( "result", k_EResultNone );
switch ( nResult )
{
case k_EResultOK:
break;
case k_EResultFail:
return; // will retry after backoff timer expires
case k_EResultNotLoggedOn:
// re-request authentication after backoff time
state.m_eState = kWebapiInventoryState_RequestAuthToken;
return;
default:
{
CUtlString strError;
pValues->GetChildStringValue( strError, "error", "" );
Warning( "Received unexpected result code %d attempting to retrieve inventory. (%s)\n", nResult, strError.Get() );
return;
}
}
// Parse the inventory message
CSteamID userSteamID( pValues->GetChildUInt64Value( "steamID" ) );
if ( !userSteamID.IsValid() || userSteamID.GetEAccountType() != k_EAccountTypeIndividual || userSteamID.GetEUniverse() != GetUniverse() )
{
Warning( "Inventory response has bad owner steam id (%s)\n", userSteamID.Render() );
return;
}
if ( pValues->FindChild( "msg" ) )
{
CUtlBuffer bufMsgSubscription;
if ( !pValues->BGetChildBinaryValue( bufMsgSubscription, "msg" ) )
{
Warning( "Inventory response missing inventory\n" );
return;
}
CGCClientSharedObjectCache *pSOCache = GetGCClient()->AddLocalSOCache( userSteamID, bufMsgSubscription.Base(), bufMsgSubscription.TellPut() );
if ( !pSOCache )
{
Warning( "Inventory response failed to create SO cache (probably protobuf didn't parse)\n" );
return;
}
// Version should match the one they said we have
Assert( pSOCache->GetVersion() == pValues->GetChildUInt64Value( "version" ) );
}
else
{
// Cache up to date. Validate version matches
CGCClientSharedObjectCache* pSOCache = GetGCClient()->FindSOCache( userSteamID, false );
Assert( pSOCache );
if( pSOCache )
{
Assert( pSOCache->GetVersion() == pValues->GetChildUInt64Value( "version" ) );
}
}
// We were successful, clear backoff timers
state.RequestSucceeded();
state.m_eState = kWebapiInventoryState_InventoryReceived;
}
void CTFGCClientSystem::SDK_SelectItemsToSendToServer( CMsgAuthorizeServerItemRetrieval* pMsg, CGCClientSharedObjectCache* pSOCache )
{
// Only send our equipped items, not the whole inventory
if ( !pSOCache )
return; // no items to send
CGCClientSharedObjectTypeCache* pItemCache = pSOCache->FindTypeCache( CEconItem::k_nTypeID );
if ( !pItemCache )
return; // no items to send
for ( uint32 i = 0; i < pItemCache->GetCount(); ++i )
{
CEconItem* pItem = ( CEconItem* )pItemCache->GetObject( i );
if ( !pItem->IsEquipped() )
continue;
pMsg->add_item_id( pItem->GetID() );
}
}
void CTFGCClientSystem::SDK_AddServerInventoryInfo( KeyValues* pKV, CGCClientSharedObjectCache* pSOCache )
{
// Here is where we would tell the DS which items we have equipped, along with any other info it needs to correctly update the socache
CGCClientSharedObjectTypeCache* pItemCache = pSOCache->FindTypeCache(CEconItem::k_nTypeID);
if (!pItemCache)
return;
CTFPlayerInventory *pLocalInv = dynamic_cast<CTFPlayerInventory*>(TFInventoryManager()->GetLocalInventory());
if (pLocalInv == NULL)
return;
// Extract our current loadout information and record it in the key values.
KeyValues *pLoadoutKV = new KeyValues("local_loadout");
for (int iClass = TF_FIRST_NORMAL_CLASS; iClass < TF_LAST_NORMAL_CLASS; iClass++)
{
char szClass[256];
V_snprintf(szClass, sizeof(szClass), "%i", iClass);
KeyValues *pClassKV = new KeyValues(szClass);
pLoadoutKV->AddSubKey(pClassKV);
for (int iSlot = 0; iSlot < CLASS_LOADOUT_POSITION_COUNT; ++iSlot)
{
CEconItemView* pItemView = TFInventoryManager()->GetItemInLoadoutForClass(iClass, iSlot);
if (!pItemView)
continue;
// todo: investigate why we get zeroes here...?
if (pItemView->GetID() == INVALID_ITEM_ID || pItemView->GetID() == 0)
continue;
char szSlot[256];
V_snprintf(szSlot, sizeof(szSlot), "%i", iSlot);
pClassKV->SetUint64(szSlot, pItemView->GetID());
}
}
pKV->AddSubKey(pLoadoutKV);
}
//-----------------------------------------------------------------------------
void CTFGCClientSystem::Update( float frametime )
{
BaseClass::Update( frametime );
WebapiInventoryThink();
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 lobby update whenever our SO cache arrives
FireGameEventLobbyUpdated();
}
}
}
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 )
{
// (CTFParty objects are handled by CTFPartyClient)
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
UpdateAssignedLobby();
// Fire this regardless of if our assigned lobby changed, since other SO fields did change.
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",
(unsigned long long) 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",
(unsigned long long) 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",
(unsigned long long) pSONotification->Obj().notification_id(),
pSONotification->Obj().notification_string().c_str() );
CClientNotification *pClientNotification = new CClientNotification();
pClientNotification->Update( pSONotification );
NotificationQueue_Add( pClientNotification );
}
}
else if ( pObject->GetTypeID() == CTFLobbyInvite::k_nTypeID )
{
OnMatchInvitesUpdated();
}
}
bool CTFGCClientSystem::UpdateAssignedLobby()
{
return false;
}
CTFParty* CTFGCClientSystem::GetParty()
{
return NULL;
}
CTFGSLobby* CTFGCClientSystem::GetLobby() const
{
return NULL;
}
//-----------------------------------------------------------------------------
bool ForceCompetitiveConvars()
{
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 )
{
Msg("Connecting to %s\n", connect );
CUtlString connectCmd;
connectCmd.Format( "connect %s matchmaking", connect );
if ( engine )
{
const bool k_bUseCompetitiveConvars = false;
bool bAllowed = !k_bUseCompetitiveConvars || 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::SetWorldStatus( CMsgTFWorldStatus &status )
{
if ( !BMessagesAreEqual( &m_WorldStatus, &status ) )
{
m_WorldStatus = status;
uint32_t nEngineVer = engine->GetClientVersion();
uint32_t nWorldVer = status.active_client_version();
// World version 0 is dev-universe for disabled checking
bool bOutOfDate = ( nWorldVer > 0 && nEngineVer != nWorldVer );
if ( !m_bClientOutOfDate && bOutOfDate )
{ Msg( "Client out of date -- world version %u, current version %u\n", nWorldVer, nEngineVer ); }
m_bClientOutOfDate = bOutOfDate;
IGameEvent *pEvent = gameeventmanager->CreateEvent( "world_status_changed" );
if ( pEvent )
{
gameeventmanager->FireEventClientSide( pEvent );
}
}
}
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;
}
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;
}
bool CTFGCClientSystem::BIsBannedFromMatchmaking( EMMPenaltyPool ePool, CRTime* prtExpireTime /* = NULL */, int* pnDuration /* = NULL */ )
{
return false;
}
#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;
}
#endif // USE_MVM_TOUR
void CTFGCClientSystem::AbandonCurrentMatch()
{
Msg( "Sending request to abandon current match\n" );
if ( BConnectedToMatchServer( true ) )
{
// If we were connected to said match, then disconnect
Msg( "Disconnecting from abandoned match server\n" );
engine->ClientCmd_Unrestricted( "disconnect" );
}
}
bool CTFGCClientSystem::IsConnectStateDisconnected()
{
return ( m_eConnectState != eConnectState_ConnectingToMatchmade &&
m_eConnectState != eConnectState_ConnectedToMatchmade );
}
//-----------------------------------------------------------------------------
// Purpose: Ask the GC for the latest global casual criteria stats
//-----------------------------------------------------------------------------
void CTFGCClientSystem::RequestMatchMakerStats() const
{
}
//-----------------------------------------------------------------------------
// 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 )
{
}
//-----------------------------------------------------------------------------
// 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;
}
//-----------------------------------------------------------------------------
// Purpose: Really here just so we can shortcircuit some staging_only debug
//-----------------------------------------------------------------------------
inline uint32 GetCountForMap( const CMsgGCMatchMakerStatsResponse msg, int nIndex )
{
return msg.map_count( nIndex );
}
//-----------------------------------------------------------------------------
// Purpose: Get the overall health of the current local casual criteria.
// Currently just takes the best individual map health.
//-----------------------------------------------------------------------------
// TODO(Universal Parties): Unused?
CTFGCClientSystem::MatchMakerHealthData_t CTFGCClientSystem::GetOverallHealthDataForLocalCriteria() const
{
uint32 nMostSearchedCount = m_nMostSearchedMapCount;
uint32 nLargestOfSelected = 0;
CCasualCriteriaHelper helper = GTFPartyClient()->GetLocalGroupCriteria().GetCasualCriteriaHelper();
// 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;
// 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 );
}
//-----------------------------------------------------------------------------
// 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::AcknowledgePendingRatingAndSources( ETFMatchGroup eMatchGroup )
{
}
void CTFGCClientSystem::AcknowledgeNotification( uint32 nAccountID, uint64 ulNotificationID )
{
}
//-----------------------------------------------------------------------------
// 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 )
{
}
void CTFGCClientSystem::ClearSurveyRequest()
{
m_msgSurveyRequest.Clear();
}
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 );
}
}
bool CTFGCClientSystem::BConnectedToMatchServer( bool bLiveMatch )
{
return false;
}
bool CTFGCClientSystem::BHaveRunningMatch() const
{
return false;
}
bool CTFGCClientSystem::BHaveLiveMatch() const
{
return false;
}
EAbandonGameStatus CTFGCClientSystem::GetAssignedMatchAbandonStatus()
{
return k_EAbandonGameStatus_Safe;
}
ETFMatchGroup CTFGCClientSystem::GetLiveMatchGroup() const
{
return k_eTFMatchGroup_Invalid;
}
void CTFGCClientSystem::JoinMMMatch()
{
}
//-----------------------------------------------------------------------------
CTFGCClientSystem::MatchInvite_t CTFGCClientSystem::GetMatchInvite( int idx ) const
{
auto *pTypeCache = m_pSOCache ? m_pSOCache->FindBaseTypeCache( CTFLobbyInvite::k_nTypeID ) : nullptr;
int nInvites = pTypeCache ? pTypeCache->GetCount() : 0;
if ( idx < 0 || idx >= nInvites )
{
AssertMsg( false, "Invalid index passed to GetMatchInvite" );
return MatchInvite_t{ 0u, k_eTFMatchGroup_Invalid, false };
}
CTFLobbyInvite *pInvite = static_cast< CTFLobbyInvite * >( pTypeCache->GetObject( idx ) );
PlayerGroupID_t nLobbyID = pInvite->GetGroupID();
ETFMatchGroup eMatchGroup = pInvite->GetMatchGroup();
bool bAcceptingThisMsg = ( m_nAcceptingMatchLobbyID == nLobbyID );
return MatchInvite_t{ nLobbyID, eMatchGroup, bAcceptingThisMsg };
}
//-----------------------------------------------------------------------------
int CTFGCClientSystem::GetMatchInviteIdxByLobbyID( PlayerGroupID_t nLobbyID ) const
{
int nNumInvites = GetNumMatchInvites();
for( int idxInvite = 0; idxInvite < nNumInvites; ++idxInvite )
{
auto invite = GetMatchInvite( idxInvite );
if ( invite.nLobbyID == nLobbyID )
{ return idxInvite; }
}
return -1;
}
//-----------------------------------------------------------------------------
int CTFGCClientSystem::GetNumMatchInvites() const
{
if ( !m_pSOCache )
{ return 0; }
CSharedObjectTypeCache *pTypeCache = m_pSOCache->FindBaseTypeCache( CTFLobbyInvite::k_nTypeID );
return pTypeCache ? pTypeCache->GetCount() : 0;
}
//-----------------------------------------------------------------------------
void CTFGCClientSystem::RequestAcceptMatchInvite( PlayerGroupID_t nLobbyID )
{
}
//-----------------------------------------------------------------------------
bool CTFGCClientSystem::BIsMatchGroupDisabled( ETFMatchGroup eMatchGroup ) const
{
for ( int i = 0; i < WorldStatus().disabled_match_groups_size(); i++ )
{
if ( WorldStatus().disabled_match_groups( i ) == eMatchGroup )
{
return true;
}
}
return false;
}
//-----------------------------------------------------------------------------
void CTFGCClientSystem::OnAcceptLobbyReply( PlayerGroupID_t )
{
Assert( m_nAcceptingMatchLobbyID != 0u );
m_nAcceptingMatchLobbyID = 0u;
// Update invites with whatever happened (the invite will be gone, or we will have failed and they'll want to see
// that bAccepting went back to false)
//
// We also suspend updating invites while an accept is in flight.
OnMatchInvitesUpdated();
}
//-----------------------------------------------------------------------------
void CTFGCClientSystem::OnMatchInvitesUpdated()
{
// Suppress while accept message in flight, OnAcceptLobbyReply will fire
if ( m_nAcceptingMatchLobbyID )
{ return; }
IGameEvent *event = gameeventmanager->CreateEvent( "match_invites_updated" );
if ( event )
{ gameeventmanager->FireEventClientSide( event ); }
}
void CTFGCClientSystem::LeaveGameAndPrepareToJoinParty( GCSDK::PlayerGroupID_t nPartyID )
{
// Leave current server
engine->ClientCmd_Unrestricted( "disconnect" );
}
bool CTFGCClientSystem::BIsPhoneVerified( void )
{
return false;
}
bool CTFGCClientSystem::BIsPhoneIdentifying( void )
{
return false;
}
bool CTFGCClientSystem::BHasCompetitiveAccess( void )
{
return false;
}