mirror of
https://github.com/alliedmodders/hl2sdk.git
synced 2026-10-12 09:58:00 +08:00
2552 lines
89 KiB
C++
2552 lines
89 KiB
C++
//========= Copyright Valve Corporation, All rights reserved. ============//
|
|
|
|
#include "cbase.h"
|
|
#include "tf_partyclient.h"
|
|
#include "confirm_dialog.h"
|
|
#include "tf_gc_shared.h"
|
|
#include "tf_gc_client.h"
|
|
#include "tf_gcmessages.pb.h"
|
|
#include "tf_party.h"
|
|
#include "tf_matchmaking_shared.h"
|
|
#include "tf_match_description.h"
|
|
#include "tf_ladder_data.h"
|
|
#include "protoutils.h"
|
|
#include "clientmode_tf.h"
|
|
#include "tier3/tier3.h"
|
|
#include "vgui_controls/URLLabel.h"
|
|
|
|
static const char* s_pszCasualCriteriaSaveFileName = "casual_criteria.vdf";
|
|
|
|
// Delay on criteria change to wait for more before sending a message
|
|
ConVar tf_mm_party_send_criteria_delay( "tf_mm_party_send_criteria_delay", "1.0", FCVAR_DEVELOPMENTONLY,
|
|
"", true, 0.f, false, 0.f );
|
|
// Minimum time to wait between criteria messages, even if it want to send them urgently, to prevent e.g. checkbox
|
|
// spamming while queued
|
|
ConVar tf_mm_party_send_criteria_delay_minimum( "tf_mm_party_send_criteria_delay_minimum", "0.5", FCVAR_DEVELOPMENTONLY,
|
|
"", true, 0.f, false, 0.f );
|
|
|
|
#ifdef _DEBUG
|
|
#define DEFAULT_PARTY_DEBUG "1"
|
|
#else
|
|
#define DEFAULT_PARTY_DEBUG "0"
|
|
#endif
|
|
|
|
ConVar tf_mm_partyclient_debug( "tf_mm_partyclient_debug", DEFAULT_PARTY_DEBUG );
|
|
|
|
static bool BPartyDbg() { return tf_mm_partyclient_debug.GetBool(); }
|
|
|
|
#define PartyDbg( ... ) \
|
|
do { \
|
|
if ( BPartyDbg() ) \
|
|
{ Msg( "[PartyClientDbg] " __VA_ARGS__ ); } \
|
|
} while(false);
|
|
|
|
#define PartyMsg( ... ) \
|
|
do { Msg( "[PartyClient] " __VA_ARGS__ ); } while(false);
|
|
|
|
#define PartyWarn( ... ) \
|
|
do { Warning( "[PartyClient] " __VA_ARGS__ ); } while(false);
|
|
|
|
static const CTFParty *ToParty( const GCSDK::CSharedObject *pObject )
|
|
{
|
|
return assert_cast< const CTFParty *>( pObject );
|
|
}
|
|
|
|
static bool AssertValidMatchGroup( ETFMatchGroup eMatchGroup )
|
|
{
|
|
if ( !ETFMatchGroup_IsValid( eMatchGroup ) || eMatchGroup == k_eTFMatchGroup_Invalid )
|
|
{
|
|
AssertMsg( false, "Bogus match group" );
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool SyncedUIStateEqual( TFSyncedMMUIState &a, TFSyncedMMUIState &b )
|
|
{
|
|
AssertMsgOnce( ValveProtoUtils::MessageHasExactFields( a, { 1, 2 } ), "Audit this if you change the message thx" );
|
|
return ( a.menu_step() == b.menu_step() ) && ( a.match_group() == b.match_group() );
|
|
}
|
|
|
|
extern CTFPartyClient *g_pTFPartyClient;
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Persistent settings
|
|
//-----------------------------------------------------------------------------
|
|
static void OnPartyClientPrefConVarChanged( IConVar *pConVar, const char *pOldString, float flOldValue )
|
|
{
|
|
auto *pPartyClient = GTFPartyClient();
|
|
if ( pPartyClient )
|
|
{
|
|
// Otherwise the client system will do this when it starts
|
|
pPartyClient->ReadConVarPreferences();
|
|
}
|
|
}
|
|
|
|
ConVar tf_mm_custom_ping_enabled( "tf_mm_custom_ping_enabled", "0", FCVAR_ARCHIVE,
|
|
"Whether to use a custom ping tolerance when matchmaking",
|
|
false, 0.f, false, 0.f, OnPartyClientPrefConVarChanged );
|
|
ConVar tf_mm_custom_ping( "tf_mm_custom_ping", "100", FCVAR_ARCHIVE,
|
|
"Custom ping tolerance in the matchmaking system. See also tf_custom_ping_enabled.",
|
|
true, (float)CUSTOM_PING_TOLERANCE_MIN, true, (float)CUSTOM_PING_TOLERANCE_MAX,
|
|
OnPartyClientPrefConVarChanged );
|
|
|
|
ConVar tf_party_join_request_mode( "tf_party_join_request_mode", "-1", FCVAR_ARCHIVE,
|
|
"The current mode for incoming party join requests:\n"
|
|
" -1 - Use default (currently 0), \n"
|
|
" 0 - Open to friends, \n"
|
|
" 1 - Friends can request to join, \n"
|
|
" 2 - Invite only\n",
|
|
true, -1.f, // Default placeholder
|
|
true, (float)CTFPartyClient::k_ePartyJoinRequestMode_Max,
|
|
OnPartyClientPrefConVarChanged );
|
|
ConVar tf_party_ignore_invites( "tf_party_ignore_invites", "0", FCVAR_ARCHIVE, "If set, ignore incoming party invites",
|
|
OnPartyClientPrefConVarChanged );
|
|
|
|
// TODO(Universal Parties): Hook up and enable in UI
|
|
ConVar tf_party_keep_on_same_team( "tf_party_keep_on_same_team", "0", FCVAR_ARCHIVE );
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Popup dialog for MM errors with an optional clickable url
|
|
//-----------------------------------------------------------------------------
|
|
class CTFMMIssueDialog : public CTFMessageBoxDialog
|
|
{
|
|
DECLARE_CLASS_SIMPLE( CTFMMIssueDialog, CTFMessageBoxDialog );
|
|
public:
|
|
CTFMMIssueDialog()
|
|
: CTFMessageBoxDialog( "", "", NULL, NULL, NULL )
|
|
{
|
|
}
|
|
|
|
virtual ~CTFMMIssueDialog() {}
|
|
|
|
virtual const char *GetResFile() OVERRIDE
|
|
{
|
|
return "Resource/UI/MMIssueDialog.res";
|
|
}
|
|
|
|
void Show( const char *pszText, const char *pszURL = nullptr )
|
|
{
|
|
wchar_t wszExpandedURL[256] = { 0 };
|
|
g_pVGuiLocalize->ConstructString_safe( m_wszText, pszText, 0 );
|
|
if ( pszURL )
|
|
{ g_pVGuiLocalize->ConstructString_safe( wszExpandedURL, pszURL, 0 ); }
|
|
SetDialogVariable( "url", wszExpandedURL );
|
|
|
|
BaseClass::Show();
|
|
|
|
vgui::URLLabel *urlLabel = dynamic_cast<vgui::URLLabel *>( FindChildByName( "URLLabel" ) );
|
|
|
|
if ( urlLabel )
|
|
{
|
|
if ( wszExpandedURL[0] )
|
|
{
|
|
char szExpandedURL[256] = { 0 };
|
|
g_pVGuiLocalize->ConvertUnicodeToANSI( wszExpandedURL, szExpandedURL, sizeof( szExpandedURL ) );
|
|
urlLabel->SetURL( szExpandedURL );
|
|
urlLabel->SetVisible( true );
|
|
}
|
|
else
|
|
{
|
|
urlLabel->SetVisible( false );
|
|
}
|
|
}
|
|
}
|
|
|
|
const wchar_t *GetText() OVERRIDE
|
|
{
|
|
return m_wszText;
|
|
}
|
|
|
|
private:
|
|
wchar_t m_wszText[256] = {0};
|
|
};
|
|
|
|
CTFMMIssueDialog *TFMMIssueDialog()
|
|
{
|
|
static vgui::DHANDLE<CTFMMIssueDialog> s_hTFMMIssueDialog;
|
|
if ( !s_hTFMMIssueDialog )
|
|
{
|
|
s_hTFMMIssueDialog = vgui::SETUP_PANEL( new CTFMMIssueDialog() );
|
|
}
|
|
|
|
return s_hTFMMIssueDialog;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Reliable messages
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Redundant header for messages following the CMsgPartyFoo/EMsgGCParty_Foo/GTFPartyClient()->OnFooReply pattern
|
|
#define RELIABLE_PARTY_MSG( name, ... ) \
|
|
class ReliableMsgParty##name : public CJobReliableMessageBase < ReliableMsgParty##name, \
|
|
CMsgParty##name, \
|
|
k_EMsgGCParty_##name, \
|
|
CMsgParty##name##Response, \
|
|
k_EMsgGCParty_##name##Response > \
|
|
{ \
|
|
public: \
|
|
const char *MsgName() { return "Party" #name ; } \
|
|
void OnReply( Reply_t &msgReply ) { GTFPartyClient()->On##name##Reply( Msg(), msgReply ); } \
|
|
void InitDebugString( CUtlString &dbgStr ) \
|
|
{ \
|
|
auto &msg = Msg().Body(); \
|
|
dbgStr.Format( __VA_ARGS__ ); \
|
|
} \
|
|
};
|
|
|
|
//-----------------------------------------------------------------------------
|
|
RELIABLE_PARTY_MSG( SetOptions, "Party ID %llu, Overwrite %d", (unsigned long long) msg.party_id(), msg.options().overwrite_existing() )
|
|
|
|
//-----------------------------------------------------------------------------
|
|
RELIABLE_PARTY_MSG( QueueForMatch, "Party ID %llu, Embedded Options %d", (unsigned long long) msg.party_id(), msg.has_final_options() )
|
|
|
|
//-----------------------------------------------------------------------------
|
|
RELIABLE_PARTY_MSG( QueueForStandby, "Party ID %llu, Lobby ID %llu", (unsigned long long) msg.party_id(), (unsigned long long) msg.party_lobby_id() )
|
|
|
|
//-----------------------------------------------------------------------------
|
|
RELIABLE_PARTY_MSG( RemoveFromQueue, "Party ID %llu", (unsigned long long) msg.party_id() )
|
|
|
|
//-----------------------------------------------------------------------------
|
|
RELIABLE_PARTY_MSG( RemoveFromStandbyQueue, "Party ID %llu", (unsigned long long) msg.party_id() )
|
|
|
|
//-----------------------------------------------------------------------------
|
|
RELIABLE_PARTY_MSG( ClearPendingPlayer, "Party ID %llu, Pending Player %s",
|
|
(unsigned long long) msg.party_id(), CSteamID( msg.pending_player_id() ).Render() )
|
|
|
|
//-----------------------------------------------------------------------------
|
|
RELIABLE_PARTY_MSG( ClearOtherPartyRequest, "Other Party ID %llu", (unsigned long long) msg.other_party_id() )
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Constructor
|
|
//-----------------------------------------------------------------------------
|
|
CTFPartyClient::CTFPartyClient()
|
|
{
|
|
g_pTFPartyClient = this;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Constructor/Destructor
|
|
//-----------------------------------------------------------------------------
|
|
CTFPartyClient::~CTFPartyClient()
|
|
{}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::Init()
|
|
{
|
|
// May have raced with convar init
|
|
m_bInit = true;
|
|
this->ReadConVarPreferences();
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CTFPerPlayerMatchCriteria &CTFPartyClient::MutLocalPlayerCriteria()
|
|
{
|
|
m_bPerPlayerCriteriaChanged = true;
|
|
m_bQueuedEffectiveCriteriaChangeEvent = true;
|
|
return m_localPerPlayerCriteria;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CTFGroupMatchCriteria &CTFPartyClient::MutLocalGroupCriteria()
|
|
{
|
|
AssertMsg( BControllingPartyActions(),
|
|
"Getting mutable group criteria when we're not in control of our group criteria" );
|
|
|
|
m_bGroupCriteriaChanged = true;
|
|
// If this is also our effective criteria, queue an event for next frame (after it presumably changes)
|
|
if ( !BEffectiveGroupCriteriaFromParty() )
|
|
{ m_bQueuedEffectiveCriteriaChangeEvent = true; }
|
|
return m_localGroupCriteria;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPartyClient::GetNumPartyMembers() const
|
|
{
|
|
// We are always a party of one if we have no active party from the perspective of outside code.
|
|
return BHaveActiveParty() ? GetActiveParty()->GetNumMembers() : 1;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CTFPartyClient::MemberStatus_t CTFPartyClient::GetPartyMemberStatus( int idx ) const
|
|
{
|
|
auto pParty = GetActiveParty();
|
|
if ( !pParty )
|
|
{
|
|
// Just us
|
|
bool bOutOfDate = GTFGCClientSystem()->BClientOutOfDate();
|
|
return MemberStatus_t{ /* bOnline */ true, bOutOfDate };
|
|
}
|
|
|
|
auto &worldStatus = GTFGCClientSystem()->WorldStatus();
|
|
auto &activity = pParty->GetMemberActivity( idx );
|
|
// Only if we've seen the client's version and gotten world status can we say they are out of date (may not during
|
|
// login). World status of 0 means no-enforcement.
|
|
bool bOutOfDate = ( worldStatus.active_client_version() != 0 && activity.has_client_version() &&
|
|
activity.client_version() != worldStatus.active_client_version() );
|
|
return MemberStatus_t{ activity.online(), bOutOfDate };
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPartyClient::CountNumOnlinePartyMembers() const
|
|
{
|
|
// We are always a party of one if we have no active party from the perspective of outside code.
|
|
auto pParty = GetActiveParty();
|
|
if ( !pParty )
|
|
{ return 1; }
|
|
|
|
// Always treat ourselves as online so the count makes a little more sense when the GC is lagged/offline
|
|
int ret = 0;
|
|
CSteamID steamIDCurrent = m_pSOCache ? m_pSOCache->GetOwner() : CSteamID();
|
|
for ( int i=0; i < pParty->GetNumMembers(); ++i )
|
|
{
|
|
if ( ( m_pSOCache && pParty->GetMember( i ) == steamIDCurrent ) || pParty->GetMemberActivity( i ).online() )
|
|
{ ++ret; }
|
|
}
|
|
|
|
return Max( ret, 1 );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CSteamID CTFPartyClient::GetPartyMember( int i ) const
|
|
{
|
|
Assert( BHaveActiveParty() );
|
|
auto pParty = GetActiveParty();
|
|
if ( !pParty )
|
|
return CSteamID();
|
|
|
|
int nCount = pParty->GetNumMembers();
|
|
Assert ( i >= 0 && i < nCount );
|
|
if ( i < 0 || i >= nCount )
|
|
return CSteamID();
|
|
|
|
return pParty->GetMember( i );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPartyClient::GetNumPendingMembers() const
|
|
{
|
|
return BHaveActiveParty() ? GetActiveParty()->GetNumPendingPlayers() : 0;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPartyClient::GetMaxPartyMembers() const
|
|
{
|
|
// There's also a GC convar mostly used for testing, but if we allowed different parties based on context/etc we
|
|
// could do that logic here.
|
|
return MAX_PARTY_SIZE;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BEffectiveGroupCriteriaFromParty() const
|
|
{
|
|
// Non-party shows local state, leaders should see their local (predicted) state rather than latency (we ensure
|
|
// these changes flush in our think)
|
|
if ( BControllingPartyActions() )
|
|
{ return false; }
|
|
|
|
AssertMsg( BHaveActiveParty(), "Should have a party if we're not in control of party actions" );
|
|
return BHaveActiveParty();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
const CTFGroupMatchCriteria &CTFPartyClient::GetEffectiveGroupCriteria() const
|
|
{
|
|
return BEffectiveGroupCriteriaFromParty() ? m_activePartyCriteria : m_localGroupCriteria;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
ConstRefTFGroupMatchCriteria CTFPartyClient::GetLastConfirmedGroupCriteria() const
|
|
{
|
|
auto *pParty = GetActiveParty();
|
|
if ( !pParty )
|
|
{
|
|
return ConstRefTFGroupMatchCriteria( m_localGroupCriteria );
|
|
}
|
|
return ConstRefTFGroupMatchCriteria( pParty->Obj().group_criteria() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::SetLocalUIState( ETFSyncedMMMenuStep eMenuStep, ETFMatchGroup eMatchGroup )
|
|
{
|
|
m_localUIState.set_menu_step( eMenuStep );
|
|
m_localUIState.set_match_group( eMatchGroup );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
const TFSyncedMMUIState& CTFPartyClient::GetLeaderUIState() const
|
|
{
|
|
// Non-party shows local state, leaders should see their local (predicted) state rather than latency (we ensure
|
|
// these changes flush in our think)
|
|
auto *pParty = GetActiveParty();
|
|
if ( !pParty || BIsPartyLeader() )
|
|
{ return m_localUIState; }
|
|
|
|
return pParty->Obj().leader_ui_state();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BInvitePlayerToParty( CSteamID steamID, bool bExpectingExistingRequestToJoin )
|
|
{
|
|
if ( !BCanInvitePlayer( steamID ) )
|
|
{
|
|
PartyWarn( "Attempting to invite user %s to party who is not in an invitable state\n",
|
|
steamID.Render() );
|
|
return false;
|
|
}
|
|
|
|
PartyMsg( "Sending request to invite %s to party %llu\n", steamID.Render(), m_unActivePartyID );
|
|
bool bSent = BSendPartyInvitePlayerMessageInternal( steamID, bExpectingExistingRequestToJoin );
|
|
if ( !bSent )
|
|
{ return false; }
|
|
|
|
// TODO(Universal Parties): Do this in a response or upon seeing the invite -- and pass party ID (and add party ID command)
|
|
if ( steamapicontext->SteamFriends() && !bExpectingExistingRequestToJoin )
|
|
{
|
|
steamapicontext->SteamFriends()->InviteUserToGame( steamID, CFmtStr( "+tf_party_request_join_user %llu 1",
|
|
steamapicontext->SteamUser()->GetSteamID().ConvertToUint64() ) );
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BSendPartyInvitePlayerMessageInternal( CSteamID steamID, bool bExpectingExistingRequestToJoin )
|
|
{
|
|
CProtoBufMsg<CMsgPartyInvitePlayer> msg( k_EMsgGCParty_InvitePlayer );
|
|
msg.Body().set_player_id( steamID.ConvertToUint64() );
|
|
|
|
if ( m_unActivePartyID )
|
|
{ msg.Body().set_party_id( m_unActivePartyID ); }
|
|
|
|
if ( bExpectingExistingRequestToJoin )
|
|
{ msg.Body().set_expecting_request_to_join( true ); }
|
|
|
|
bool bSent = GCClientSystem()->BSendMessage( msg );
|
|
|
|
if ( !bSent )
|
|
{
|
|
PartyWarn("Failed to send party invite message for %s\n", steamID.Render() );
|
|
}
|
|
return bSent;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BRequestJoinPlayer( CSteamID steamID, bool bExpectingExistingInvite )
|
|
{
|
|
if ( !BCanRequestToJoinPlayer( steamID ) )
|
|
{
|
|
PartyWarn( "Cannot request to join user %s to party who is not in an invitable state\n",
|
|
steamID.Render() );
|
|
return false;
|
|
}
|
|
|
|
PartyMsg( "Sending request to join %s party (current party %llu)\n", steamID.Render(), m_unActivePartyID );
|
|
CProtoBufMsg<CMsgPartyRequestJoinPlayer> msg( k_EMsgGCParty_RequestJoinPlayer );
|
|
if ( m_unActivePartyID )
|
|
{ msg.Body().set_current_party_id( m_unActivePartyID ); }
|
|
msg.Body().set_join_player_id( steamID.ConvertToUint64() );
|
|
if ( bExpectingExistingInvite )
|
|
{ msg.Body().set_expecting_invite( true ); }
|
|
GCClientSystem()->BSendMessage( msg );
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BCanInvitePlayer( CSteamID steamID )
|
|
{
|
|
// Note that auto-invites/rejects check AllowedToPartyWith directly as we never actually have a visible incoming
|
|
// join request
|
|
if ( !BControllingPartyActions() || !BAllowedToPartyWith( steamID ) )
|
|
{ return false; }
|
|
|
|
// Must have a free slot unless they are a pending join request already
|
|
return ( GetNumFreePartySlots() > 0 ) || BHaveIncomingJoinRequestForSteamID( steamID );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::CancelOutgoingInviteOrIncomingJoinRequest( CSteamID steamIDTarget )
|
|
{
|
|
// TODO(Universal Parties): Add local state prediction, (wrap TFParty getters)
|
|
if ( !BControllingPartyActions() )
|
|
{ return; }
|
|
|
|
PartyMsg( "Sending request to cancel outgoing invite/incoming join request for %s (current party %llu)\n",
|
|
steamIDTarget.Render(), m_unActivePartyID );
|
|
CProtoBufMsg<CMsgPartyClearPendingPlayer> msg( k_EMsgGCParty_ClearPendingPlayer );
|
|
if ( m_unActivePartyID )
|
|
{ msg.Body().set_party_id( m_unActivePartyID ); }
|
|
msg.Body().set_pending_player_id( steamIDTarget.ConvertToUint64() );
|
|
GCClientSystem()->BSendMessage( msg );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::CancelOutgoingJoinRequestOrIncomingInvite( CSteamID steamIDTarget )
|
|
{
|
|
// TODO(Universal Parties): Add local state prediction in getters
|
|
PartyMsg( "Sending request to cancel outgoing invite/incoming join request for %s (current party %llu)\n",
|
|
steamIDTarget.Render(), m_unActivePartyID );
|
|
CProtoBufMsg<CMsgPartyClearOtherPartyRequest> msg( k_EMsgGCParty_ClearOtherPartyRequest );
|
|
|
|
// Note that our local lists are the lists the user should see and include prediction -- we want to check the raw
|
|
// object for an invite that can be rejected.
|
|
GCSDK::PlayerGroupID_t nOtherParty = 0u;
|
|
CSharedObjectTypeCache *pTypeCache = ( m_pSOCache ? m_pSOCache->FindBaseTypeCache( CTFPartyInvite::k_nTypeID ) \
|
|
: nullptr );
|
|
if ( pTypeCache )
|
|
{
|
|
int nCount = pTypeCache->GetCount();
|
|
for ( int i = 0; i < nCount; i++ )
|
|
{
|
|
CTFPartyInvite *pInvite = static_cast< CTFPartyInvite * >( pTypeCache->GetObject( i ) );
|
|
if ( pInvite->GetInviter() == steamIDTarget )
|
|
{ nOtherParty = pInvite->GetGroupID(); }
|
|
}
|
|
}
|
|
|
|
if ( nOtherParty == 0u )
|
|
{
|
|
PartyWarn( "CTFPartyClient::CancelOutgoingJoinRequestOrIncomingInvite for %s -- "
|
|
"could not find an invite related to that user\n",
|
|
steamIDTarget.Render() );
|
|
return;
|
|
}
|
|
|
|
PartyDbg( "Sending CancelOutgoingJoinRequestOrIncomingInvite(%llu)\n", nOtherParty );
|
|
msg.Body().set_other_party_id( nOtherParty );
|
|
GCClientSystem()->BSendMessage( msg );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BPromoteToLeader( CSteamID steamID )
|
|
{
|
|
if ( !BControllingPartyActions() || !BHaveActiveParty() )
|
|
{ return false; }
|
|
|
|
auto *pParty = GetActiveParty();
|
|
if ( pParty->GetMemberIndexBySteamID( steamID ) == -1 )
|
|
{ return false; }
|
|
|
|
// Not really worth predicting
|
|
CProtoBufMsg<CMsgPartyPromoteToLeader> msg( k_EMsgGCParty_PromoteToLeader );
|
|
msg.Body().set_party_id( m_unActivePartyID );
|
|
msg.Body().set_new_leader_id( steamID.ConvertToUint64() );
|
|
|
|
PartyDbg( "Sending PartyPromoteToLeader(%s)\n", steamID.Render() );
|
|
GCClientSystem()->BSendMessage( msg );
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BKickPartyMember( CSteamID steamID )
|
|
{
|
|
if ( !BControllingPartyActions() || !BHaveActiveParty() )
|
|
{ return false; }
|
|
|
|
auto *pParty = GetActiveParty();
|
|
if ( pParty->GetMemberIndexBySteamID( steamID ) == -1 )
|
|
{ return false; }
|
|
|
|
// TODO(Universal Parties): Predict
|
|
CProtoBufMsg<CMsgPartyKickMember> msg( k_EMsgGCParty_KickMember );
|
|
msg.Body().set_party_id( m_unActivePartyID );
|
|
msg.Body().set_target_id( steamID.ConvertToUint64() );
|
|
|
|
PartyDbg( "Sending PartyKickMember(%s)\n", steamID.Render() );
|
|
GCClientSystem()->BSendMessage( msg );
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BCanRequestToJoinPlayer( CSteamID steamID )
|
|
{
|
|
// UpdateActiveParty should only allow invites we're allowed to act upon. If we have no invite, we can only
|
|
// initiate with players that are valid party targets.
|
|
//
|
|
// Note that auto-invites/rejects check AllowedToPartyWith directly as we never actually have a visible incoming
|
|
// join request
|
|
return BAllowedToPartyWith( steamID ) || BHaveIncomingInviteForSteamID( steamID );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BAllowedToPartyWith( CSteamID steamID ) const
|
|
{
|
|
auto *pParty = GetActiveParty();
|
|
if ( pParty )
|
|
{
|
|
if ( pParty->GetMemberIndexBySteamID( steamID ) != -1 )
|
|
return false;
|
|
}
|
|
|
|
auto *pFriends = steamapicontext->SteamFriends();
|
|
Assert( pFriends );
|
|
|
|
// Check this logic if new values are added
|
|
COMPILE_TIME_ASSERT( k_EFriendRelationshipMax == 8 && k_EPersonaStateMax == 8 );
|
|
|
|
// Only friends for now
|
|
return ( pFriends &&
|
|
pFriends->GetFriendRelationship( steamID ) == k_EFriendRelationshipFriend );
|
|
|
|
// Not checking this -- friend may be invisible but still invitable (user won't know if their invite succeeded
|
|
// though)
|
|
//
|
|
// pFriends->GetFriendPersonaState( steamID ) != k_EPersonaStateOffline
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BCurrentMatchOrInviteDisallowsQueuing( bool bCheckParty /* = true */ ) const
|
|
{
|
|
auto *pParty = GetActiveParty();
|
|
if ( !pParty || !m_pSOCache )
|
|
{ return false; }
|
|
|
|
CSteamID steamID = m_pSOCache->GetOwner();
|
|
for ( int idx = 0; idx < pParty->GetNumMembers(); idx++ )
|
|
{
|
|
if ( !bCheckParty && pParty->GetMember( idx ) != steamID )
|
|
{ continue; }
|
|
if ( pParty->GetMemberActivity( idx ).multiqueue_blocked() )
|
|
{ return true; }
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BCanQueueForMatch( ETFMatchGroup eGroup, CUtlVector< QueueEligibilityData_t >& vecReasons ) const
|
|
{
|
|
auto lambdaCopyWReason = [&]( const wchar_t* pwszReason, EDisabledReasonType eType )
|
|
{
|
|
auto pNewReason = vecReasons.AddToTailGetPtr();
|
|
V_wcsncpy( pNewReason->wszCantReason, pwszReason, sizeof( pNewReason->wszCantReason ) );
|
|
pNewReason->m_eReason = eType;
|
|
};
|
|
|
|
auto lambdaCopyReason = [&]( const char* pszLocToken, EDisabledReasonType eType )
|
|
{
|
|
auto *pNewReason = vecReasons.AddToTailGetPtr();
|
|
wchar_t *pwszReason = g_pVGuiLocalize->Find( pszLocToken );
|
|
if ( pwszReason )
|
|
{ V_wcscpy_safe( pNewReason->wszCantReason, pwszReason ); }
|
|
pNewReason->m_eReason = eType;
|
|
};
|
|
|
|
auto *pMatchDesc = GetMatchGroupDescription( eGroup );
|
|
if ( !pMatchDesc )
|
|
return false;
|
|
|
|
// Check for matchmaking bans and display time remaining if we're banned
|
|
if ( steamapicontext == NULL || steamapicontext->SteamUser() == NULL )
|
|
return false;
|
|
|
|
// Global state preventing?
|
|
{
|
|
if ( !GTFGCClientSystem()->BHealthyGCConnection() )
|
|
{
|
|
lambdaCopyReason( "#TF_MM_NoGC", k_eDisabledType_Network );
|
|
}
|
|
|
|
// Is this match group disabled?
|
|
if ( GTFGCClientSystem()->BIsMatchGroupDisabled( eGroup ) )
|
|
{
|
|
lambdaCopyReason( "#TF_Competitive_MatchTypeDisabled", k_eDisabledType_System );
|
|
}
|
|
|
|
// Are we already queued for it?
|
|
if ( GTFPartyClient()->BInQueueForMatchGroup( eGroup ) )
|
|
{
|
|
lambdaCopyReason( "#TF_MM_AlreadyQueued", k_eDisabledType_System );
|
|
}
|
|
|
|
// If we have a match that doesn't allow for queuing while playing, then prevent queuing
|
|
if ( BCurrentMatchOrInviteDisallowsQueuing( false ) )
|
|
{
|
|
lambdaCopyReason( "#TF_Competitive_MatchRunning", k_eDisabledType_Locked );
|
|
}
|
|
|
|
if ( BCurrentMatchOrInviteDisallowsQueuing( true ) )
|
|
{
|
|
lambdaCopyReason( "#TF_Competitive_PartyMatchRunning", k_eDisabledType_Locked );
|
|
}
|
|
|
|
if ( !BControllingPartyActions() )
|
|
{
|
|
lambdaCopyReason( "#TF_Matchmaking_OnlyLeaderCanQueue", k_eDisabledType_Criteria );
|
|
}
|
|
}
|
|
|
|
auto pParty = GetActiveParty();
|
|
|
|
// Match type criteria preventing?
|
|
switch ( eGroup )
|
|
{
|
|
case k_eTFMatchGroup_Casual_12v12:
|
|
{
|
|
// Need to have at least one map selected
|
|
if ( !GetEffectiveGroupCriteria().GetCasualCriteriaHelper().AnySelected() )
|
|
{
|
|
lambdaCopyReason( "#TF_Matchmaking_CantQueue_NoCasualCriteria", k_eDisabledType_Criteria );
|
|
}
|
|
}
|
|
break;
|
|
|
|
case k_eTFMatchGroup_Event_Placeholder:
|
|
case k_eTFMatchGroup_Ladder_6v6:
|
|
{
|
|
|
|
}
|
|
break;
|
|
|
|
case k_eTFMatchGroup_MvM_MannUp:
|
|
{
|
|
if ( pParty ? pParty->BAnyMemberWithoutTicket() : !GTFGCClientSystem()->BLocalPlayerInventoryHasMvmTicket() )
|
|
{
|
|
lambdaCopyReason( "#TF_Matchmaking_CantQueue_NoTicket", k_eDisabledType_Criteria );
|
|
}
|
|
}
|
|
// Intentionally fall through for the criteria check
|
|
|
|
case k_eTFMatchGroup_MvM_Practice:
|
|
{
|
|
// Need to have at least one map
|
|
CMvMMissionSet searchChallenges;
|
|
GetEffectiveGroupCriteria().GetMvMMissionSet( searchChallenges, eGroup == k_eTFMatchGroup_MvM_MannUp );
|
|
if ( searchChallenges.IsEmpty() )
|
|
{
|
|
lambdaCopyReason( "#TF_Matchmaking_CantQueue_NoMvMCriteria", k_eDisabledType_Criteria );
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Unhandled match type!
|
|
Assert( false );
|
|
}
|
|
|
|
// Party state preventing?
|
|
{
|
|
auto &worldStatus = GTFGCClientSystem()->WorldStatus();
|
|
|
|
if ( pParty )
|
|
{
|
|
for ( int i=0; i < pParty->GetNumMembers(); ++i )
|
|
{
|
|
auto &activity = pParty->GetMemberActivity( i );
|
|
// Only if we've seen the client's version and gotten world status can we say they are out of date (may not
|
|
// during login). World status of 0 means no-enforcement.
|
|
bool bOutOfDate = ( worldStatus.active_client_version() != 0 && activity.has_client_version() &&
|
|
activity.client_version() != worldStatus.active_client_version() );
|
|
if ( bOutOfDate )
|
|
{ lambdaCopyReason( "#TF_Competitive_PartyMemberOutOfDate", k_eDisabledType_Network ); }
|
|
if ( !activity.online() )
|
|
{ lambdaCopyReason( "#TF_Competitive_PartyMemberOffline", k_eDisabledType_Network ); }
|
|
}
|
|
}
|
|
|
|
EMMPenaltyPool ePenaltyPool = pMatchDesc->GetPenaltyPool();
|
|
|
|
// Check to see if we are banned
|
|
{
|
|
CRTime timeExpire = CRTime::RTime32TimeCur();
|
|
int nDuration = -1;
|
|
bool bBanned = GTFGCClientSystem()->BIsBannedFromMatchmaking( ePenaltyPool, &timeExpire, &nDuration );
|
|
|
|
if ( bBanned )
|
|
{
|
|
lambdaCopyReason( "#TF_Matchmaking_CantQueue_Ban", k_eDisabledType_Banned );
|
|
}
|
|
}
|
|
|
|
// Check if a party member is banned
|
|
if ( pParty && pParty->BAnyMembersBanned( ePenaltyPool ) )
|
|
{
|
|
CUtlVector< CSteamID > vecBannedSteamIDs;
|
|
for( int i=0; i < pParty->GetNumMembers(); ++i )
|
|
{
|
|
CSteamID steamIDMember = pParty->GetMember( i );
|
|
if ( pParty->BMembersIsBanned( steamIDMember, ePenaltyPool ) )
|
|
{
|
|
vecBannedSteamIDs.AddToTail( steamIDMember );
|
|
}
|
|
}
|
|
|
|
Assert( vecBannedSteamIDs.Count() );
|
|
|
|
BlameNames_t bannedNames( vecBannedSteamIDs,
|
|
"#TF_Matchmaking_CantQueue_BanParty",
|
|
"#TF_Matchmaking_CantQueue_VerbSingle",
|
|
"#TF_Matchmaking_CantQueue_VerbPlural" );
|
|
lambdaCopyWReason( bannedNames.Get(), k_eDisabledType_Banned );
|
|
}
|
|
|
|
// Someone in the party doesn't have comp access
|
|
if ( pMatchDesc->BRequiresCompetitiveAccess() )
|
|
{
|
|
// We don't have comp access
|
|
if( !GTFGCClientSystem()->BHasCompetitiveAccess() )
|
|
{
|
|
lambdaCopyReason( "#TF_Competitive_Requirements", k_eDisabledType_Locked );
|
|
}
|
|
|
|
// Someone else doesn't have comp access
|
|
if( pParty && pParty->BAnyMemberWithoutCompetitiveAccess() )
|
|
{
|
|
CUtlVector< CSteamID > vecBlameSteamIDs;
|
|
for( int i=0; i < pParty->GetNumMembers(); ++i )
|
|
{
|
|
CSteamID steamIDMember = pParty->GetMember( i );
|
|
if ( pParty->BMemberWithoutCompetitiveAccess( steamIDMember ) )
|
|
{
|
|
vecBlameSteamIDs.AddToTail( steamIDMember );
|
|
}
|
|
}
|
|
|
|
BlameNames_t restrictedNames( vecBlameSteamIDs,
|
|
"#TF_Competitive_Requirements_Party",
|
|
"#TF_PartyMemberState_Singular",
|
|
"#TF_PartyMemberState_Plural" );
|
|
lambdaCopyWReason( restrictedNames.Get(), k_eDisabledType_Locked );
|
|
}
|
|
}
|
|
}
|
|
|
|
vecReasons.SortPredicate( []( const QueueEligibilityData_t& left, const QueueEligibilityData_t& right )
|
|
{
|
|
return left.m_eReason > right.m_eReason;
|
|
} );
|
|
|
|
return vecReasons.IsEmpty();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::RequestQueueForMatch( ETFMatchGroup eMatchGroup )
|
|
{
|
|
// Can't re-queue until we're confirmed out of queue, see BInQueue semantics
|
|
if ( BHavePendingQueueMsg( eMatchGroup ) || BInQueueForMatchGroup( eMatchGroup ) )
|
|
{ return; }
|
|
|
|
auto *pReliable = new ReliableMsgPartyQueueForMatch;
|
|
pReliable->Msg().Body().set_match_group( eMatchGroup );
|
|
if ( m_unActivePartyID )
|
|
{ pReliable->Msg().Body().set_party_id( m_unActivePartyID ); }
|
|
|
|
// Append any un-sent options so we are never in queue, however briefly, with options we had selected prior to
|
|
// requesting queuing. We may also have a pending SetOptions message, that's okay, since these will stack with that
|
|
// and come in afterwards.
|
|
CTFPartyOptions pendingOptions;
|
|
if ( BMakeUpdateMsg( pendingOptions ) )
|
|
{ pReliable->Msg().Body().mutable_final_options()->CopyFrom( pendingOptions ); }
|
|
|
|
PartyMsg( "Requesting queue for %s\n", GetMatchGroupName( eMatchGroup ) );
|
|
PartyDbg( "Sending PartyQueueForMatch:\n%s", pReliable->Msg().Body().DebugString().c_str() );
|
|
GTFGCClientSystem()->ReliableMsgQueue().Enqueue( pReliable );
|
|
SetPendingQueueMsg( eMatchGroup, true );
|
|
// See comment in UpdateActiveParty about when InQueue changes.
|
|
if ( !BInQueueForMatchGroup( eMatchGroup ) )
|
|
{
|
|
// Force a party update, since we predict in-queue state when we have a pending message. Otherwise the state
|
|
// change wouldn't fire until the GC acknowledged things.
|
|
UpdateActiveParty();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::RequestQueueForStandby()
|
|
{
|
|
// Can't re-queue until we're confirmed out of queue, see BInQueue semantics
|
|
if ( !BHaveActiveParty() || m_bPendingStandbyQueueMsg || !BCanQueueForStandby() || BInStandbyQueue() )
|
|
{ return; }
|
|
|
|
auto *pReliable = new ReliableMsgPartyQueueForStandby;
|
|
pReliable->Msg().Body().set_party_id( m_unActivePartyID );
|
|
pReliable->Msg().Body().set_party_lobby_id( GetActiveParty()->GetAssociatedLobbyID() );
|
|
|
|
PartyMsg( "Requesting queue to join party's lobby\n" );
|
|
PartyDbg( "Sending PartyQueueForStandby:\n%s", pReliable->Msg().Body().DebugString().c_str() );
|
|
GTFGCClientSystem()->ReliableMsgQueue().Enqueue( pReliable );
|
|
m_bPendingStandbyQueueMsg = true;
|
|
// See comment in UpdateActiveParty about when InQueue changes
|
|
if ( !BInStandbyQueue() )
|
|
{
|
|
m_bInStandbyQueue = true;
|
|
OnInStandbyQueueChanged();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::CancelMatchQueueRequest( ETFMatchGroup eMatchGroup )
|
|
{
|
|
// BInQueue remains true until we're confirmed out of queue, see BInQueue semantics comment
|
|
if ( !BInQueueForMatchGroup( eMatchGroup ) || BHavePendingQueueCancelMsg( eMatchGroup ) )
|
|
{ return; }
|
|
|
|
// TODO(Universal Parties): Race/prediction?
|
|
auto *pReliable = new ReliableMsgPartyRemoveFromQueue;
|
|
pReliable->Msg().Body().set_match_group( eMatchGroup );
|
|
if ( m_unActivePartyID )
|
|
{ pReliable->Msg().Body().set_party_id( m_unActivePartyID ); }
|
|
|
|
PartyMsg( "Requesting exit queue for %s\n", GetMatchGroupName( eMatchGroup ) );
|
|
PartyDbg( "Sending PartyRemoveFromQueue:\n%s", pReliable->Msg().Body().DebugString().c_str() );
|
|
SetPendingQueueCancelMsg( eMatchGroup, true );
|
|
GTFGCClientSystem()->ReliableMsgQueue().Enqueue( pReliable );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::CancelStandbyQueueRequest()
|
|
{
|
|
// BInQueue remains true until we're confirmed out of queue, see BInQueue semantics comment
|
|
if ( !BInStandbyQueue() || m_bPendingStandbyQueueCancelMsg )
|
|
{ return; }
|
|
|
|
// TODO(Universal Parties): Race/prediction?
|
|
auto *pReliable = new ReliableMsgPartyRemoveFromStandbyQueue;
|
|
if ( m_unActivePartyID )
|
|
{ pReliable->Msg().Body().set_party_id( m_unActivePartyID ); }
|
|
|
|
PartyMsg( "Requesting exit standby queue\n" );
|
|
PartyDbg( "Sending PartyRemoveFromStandbyQueue:\n%s", pReliable->Msg().Body().DebugString().c_str() );
|
|
m_bPendingStandbyQueueCancelMsg = true;
|
|
GTFGCClientSystem()->ReliableMsgQueue().Enqueue( pReliable );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::InternalSetInQueueForMatchGroup( ETFMatchGroup eMatchGroup, bool bSet )
|
|
{
|
|
if ( !AssertValidMatchGroup( eMatchGroup ) )
|
|
{ return; }
|
|
|
|
int idxQueue = m_vecInQueue.Find( eMatchGroup );
|
|
bool bQueued = idxQueue != m_vecInQueue.InvalidIndex();
|
|
if ( bQueued && !bSet )
|
|
{ m_vecInQueue.Remove( idxQueue ); }
|
|
else if ( bSet && !bQueued )
|
|
{ m_vecInQueue.AddToTail( eMatchGroup ); }
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BHaveAnyPendingQueueMsg() const
|
|
{
|
|
for ( int iMatchGroup = k_eTFMatchGroup_First; iMatchGroup < ETFMatchGroup_ARRAYSIZE; iMatchGroup++ )
|
|
{
|
|
if ( !ETFMatchGroup_IsValid( iMatchGroup ) )
|
|
{ continue; }
|
|
if ( m_arbPendingQueueMsg[iMatchGroup] )
|
|
{ return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BHavePendingQueueMsg( ETFMatchGroup eMatchGroup ) const
|
|
{
|
|
if ( !AssertValidMatchGroup( eMatchGroup ) )
|
|
{ return false; }
|
|
|
|
return m_arbPendingQueueMsg[eMatchGroup];
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::SetPendingQueueMsg( ETFMatchGroup eMatchGroup, bool bSet )
|
|
{
|
|
if ( !AssertValidMatchGroup( eMatchGroup ) )
|
|
{ return; }
|
|
|
|
m_arbPendingQueueMsg[eMatchGroup] = bSet;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BHavePendingQueueCancelMsg( ETFMatchGroup eMatchGroup ) const
|
|
{
|
|
if ( !AssertValidMatchGroup( eMatchGroup ) )
|
|
{ return false; }
|
|
|
|
return m_arbPendingQueueCancelMsg[eMatchGroup];
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::SetPendingQueueCancelMsg( ETFMatchGroup eMatchGroup, bool bSet )
|
|
{
|
|
if ( !AssertValidMatchGroup( eMatchGroup ) )
|
|
{ return; }
|
|
|
|
m_arbPendingQueueCancelMsg[eMatchGroup] = bSet;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::SaveCasualCriteria()
|
|
{
|
|
GetLocalGroupCriteria().SaveCasualCriteriaToFile( s_pszCasualCriteriaSaveFileName );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::LoadSavedCasualCriteria()
|
|
{
|
|
MutLocalGroupCriteria().LoadCasualCriteriaFromFile( s_pszCasualCriteriaSaveFileName );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::ReadConVarPreferences()
|
|
{
|
|
if ( !m_bInit )
|
|
{ return; }
|
|
|
|
// Custom ping
|
|
{
|
|
bool bEnabled = tf_mm_custom_ping_enabled.GetBool();
|
|
uint32 unValue = bEnabled ? (uint32)Max( 0, tf_mm_custom_ping.GetInt() ) : 0u;
|
|
|
|
MutLocalGroupCriteria().SetCustomPingTolerance( unValue );
|
|
}
|
|
|
|
// Party join-request mode
|
|
int nRawJoinRequestMode = tf_party_join_request_mode.GetInt();
|
|
if ( nRawJoinRequestMode == -1 )
|
|
{
|
|
SetPartyJoinRequestMode( k_ePartyJoinRequestMode_Current_Default );
|
|
}
|
|
else
|
|
{
|
|
SetPartyJoinRequestMode( (EPartyJoinRequestMode)Clamp( nRawJoinRequestMode,
|
|
(int)k_ePartyJoinRequestMode_Min,
|
|
(int)k_ePartyJoinRequestMode_Max ) );
|
|
}
|
|
|
|
// Ignore incoming invites
|
|
SetIgnorePartyInvites( tf_party_ignore_invites.GetBool() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CTFPartyClient::EPartyJoinRequestMode CTFPartyClient::GetPartyJoinRequestMode() const
|
|
{
|
|
return m_ePrefPartyJoinRequestMode;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::SetPartyJoinRequestMode( EPartyJoinRequestMode eMode )
|
|
{
|
|
if ( m_ePrefPartyJoinRequestMode != eMode )
|
|
{
|
|
m_ePrefPartyJoinRequestMode = eMode;
|
|
// This will trigger a OnChanged and call us again, make sure we set the mode first so we don't get in a loop.
|
|
tf_party_join_request_mode.SetValue( (int)eMode );
|
|
OnPartyPrefChanged();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::GetIgnorePartyInvites() const
|
|
{
|
|
return m_bPrefIgnoreInvites;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::SetIgnorePartyInvites( bool bIgnore )
|
|
{
|
|
if ( m_bPrefIgnoreInvites != bIgnore )
|
|
{
|
|
m_bPrefIgnoreInvites = bIgnore;
|
|
// This will trigger a OnChanged and call us again, make sure we set the mode first so we don't get in a loop.
|
|
tf_party_ignore_invites.SetValue( bIgnore );
|
|
OnPartyPrefChanged();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BInQueueForMatchGroup( ETFMatchGroup eMatchGroup ) const
|
|
{
|
|
// See UpdateActiveParty for where the edges of this are defined w.r.t. pending messages
|
|
if ( !AssertValidMatchGroup( eMatchGroup ) )
|
|
{ return false; }
|
|
|
|
return m_vecInQueue.Find( eMatchGroup ) != m_vecInQueue.InvalidIndex();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPartyClient::GetNumQueuedMatchGroups() const
|
|
{
|
|
return m_vecInQueue.Count();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
ETFMatchGroup CTFPartyClient::GetQueuedMatchGroupByIdx( int idx ) const
|
|
{
|
|
Assert( idx >= 0 && idx < m_vecInQueue.Count() );
|
|
if ( idx < 0 || idx >= m_vecInQueue.Count() )
|
|
return k_eTFMatchGroup_Invalid;
|
|
|
|
return m_vecInQueue[ idx ];
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BInStandbyQueue() const
|
|
{
|
|
// See UpdateActiveParty for where the edges of this are defined w.r.t. pending messages
|
|
return m_bInStandbyQueue;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BCanQueueForStandby() const
|
|
{
|
|
// Don't need to check the party since standby queue is per-individual
|
|
if ( BCurrentMatchOrInviteDisallowsQueuing( /* bCheckParty */ false ) || BInStandbyQueue() || !BHaveActiveParty() )
|
|
{ return false; }
|
|
|
|
// Does our party have a match?
|
|
PlayerGroupID_t nPartyAssociatedLobby = GetActiveParty()->GetAssociatedLobbyID();
|
|
if ( nPartyAssociatedLobby == 0u )
|
|
{ return false; }
|
|
|
|
// Does it allow this?
|
|
auto *pMatchDesc = GetMatchGroupDescription( GetActiveParty()->GetAssociatedLobbyMatchGroup() );
|
|
if ( !pMatchDesc || !pMatchDesc->BAllowsPartyJoins() )
|
|
{ return false; }
|
|
|
|
// Are we already in it? (Note that we *can* queue for standby if we're in *some other* match)
|
|
if ( GTFGCClientSystem()->BHaveLiveMatch() && GTFGCClientSystem()->GetLiveMatchLobbyID() == nPartyAssociatedLobby )
|
|
{ return false; }
|
|
|
|
// Are we already invited to it?
|
|
for ( int idxInvite = 0; idxInvite < GTFGCClientSystem()->GetNumMatchInvites(); idxInvite++ )
|
|
{
|
|
if ( GTFGCClientSystem()->GetMatchInvite( idxInvite ).nLobbyID == nPartyAssociatedLobby )
|
|
{ return false; }
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::Think()
|
|
{
|
|
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ );
|
|
if ( m_bQueuedEffectiveCriteriaChangeEvent )
|
|
{ OnEffectiveCriteriaChanged(); }
|
|
|
|
CheckSendUpdates();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::CheckSendUpdates()
|
|
{
|
|
// Should we flush any changes asap rather than lazily buffering them. If we are in queue and this actually affects
|
|
// us, we want to flush all changes, but otherwise we will lazily buffer them.
|
|
bool bSyncUrgent = BInAnyMatchQueue();
|
|
// Are we in a state where we want to keep our criteria sync'd with the GC at all?
|
|
bool bSync = ( GetNumPartyMembers() > 1 ) || bSyncUrgent;
|
|
|
|
// No action
|
|
if ( !bSync )
|
|
{ return; }
|
|
|
|
// Anything to send?
|
|
uint32_t dirtyFlags = CalcDirtyFlags();
|
|
if ( m_bSentInitialCriteria && dirtyFlags == eDirty_None )
|
|
{ return; }
|
|
|
|
//
|
|
// Dirty criteria and we want to be in sync, see if we should send an update now
|
|
//
|
|
|
|
// Track how long changes have been pending
|
|
if ( m_flPendingChangesTime == -1.f )
|
|
{ m_flPendingChangesTime = Plat_FloatTime(); }
|
|
|
|
auto &cvDelay = tf_mm_party_send_criteria_delay;
|
|
auto &cvMinDelay = tf_mm_party_send_criteria_delay_minimum;
|
|
auto &flLastUpdate = m_flLastCriteriaUpdate;
|
|
bool bSendNow = ( m_flPendingChangesTime != -1.f &&
|
|
// Not if there's still reliable criteria in queue
|
|
!m_bPendingReliableCriteriaMsg &&
|
|
// Wait for the nominal delay unless its urgent
|
|
( bSyncUrgent || m_flPendingChangesTime + cvDelay.GetFloat() < Plat_FloatTime() ) &&
|
|
// But always wait for the minimum delay (if the user is e.g. spamming a checkbox)
|
|
( flLastUpdate == -1.f || ( flLastUpdate + cvMinDelay.GetFloat() < Plat_FloatTime() ) ) );
|
|
|
|
if ( !bSendNow )
|
|
{ return; }
|
|
|
|
//
|
|
// Build update
|
|
//
|
|
|
|
|
|
auto *pReliable = new ReliableMsgPartySetOptions;
|
|
pReliable->Msg().Body().set_party_id( m_unActivePartyID );
|
|
CTFPartyOptions *pOptions = pReliable->Msg().Body().mutable_options();
|
|
|
|
bool bMadeOptions = BMakeUpdateMsg( *pOptions );
|
|
if ( !bMadeOptions )
|
|
{
|
|
// This is fine, we may have marked criteria dirty but then changed it back to the previous state, resulting in
|
|
// a no-op.
|
|
PartyDbg( "CheckSendUpdates - Had dirty state, but turned out to be in sync when crafting update\n" );
|
|
m_flPendingChangesTime = -1.f;
|
|
return;
|
|
}
|
|
|
|
// Send
|
|
PartyDbg( "Sending Party SetOptions:\n%s", pReliable->Msg().Body().DebugString().c_str() );
|
|
m_flLastCriteriaUpdate = Plat_FloatTime();
|
|
m_flPendingChangesTime = -1.f;
|
|
GTFGCClientSystem()->ReliableMsgQueue().Enqueue( pReliable );
|
|
m_bPendingReliableCriteriaMsg = true;
|
|
m_unPendingReliableCriteriaMsgParty = m_unActivePartyID;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
uint32_t CTFPartyClient::CalcDirtyFlags()
|
|
{
|
|
// Note we don't consider !m_bSentInitial dirty -- we set the overwrite flag in our initial update, and
|
|
// !m_bSentInitial forces an initial update, so the server *will* match our last sent after that. (We clear last
|
|
// sent to defaults in CheckResetSentOptions)
|
|
uint32_t dirtyFlags = eDirty_None;
|
|
if ( !SyncedUIStateEqual( m_lastSentUIState, m_localUIState ) && BIsPartyLeader() )
|
|
{ dirtyFlags |= eDirty_UIState; }
|
|
|
|
if ( BControllingPartyActions() && m_bGroupCriteriaChanged )
|
|
{ dirtyFlags |= eDirty_GroupCriteria; }
|
|
if ( m_bPerPlayerCriteriaChanged )
|
|
{ dirtyFlags |= eDirty_PlayerCriteria; }
|
|
|
|
return dirtyFlags;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BMakeUpdateMsg( CTFPartyOptions &msgOptions )
|
|
{
|
|
uint32_t dirtyFlags = CalcDirtyFlags();
|
|
|
|
#if defined( _DEBUG )
|
|
// Debug only - Guard against forgetting to set dirty flags somewhere
|
|
if ( !( dirtyFlags & ( eDirty_PlayerCriteria | eDirty_GroupCriteria ) ) )
|
|
{
|
|
CTFGroupMatchCriteria groupDelta;
|
|
CTFPerPlayerMatchCriteria perPlayerDelta;
|
|
bool bCheckGroup = BControllingPartyActions(); // We purposefully ignore changes when not in control
|
|
bool bGroupChanges = bCheckGroup && groupDelta.MakeDelta( m_lastSentGroupCriteria, m_localGroupCriteria );
|
|
bool bPlayerChanges = perPlayerDelta.MakeDelta( m_lastSentPerPlayerCriteria, m_localPerPlayerCriteria );
|
|
AssertMsg( !bGroupChanges && !bPlayerChanges, "Criteria was not marked changed but changed" );
|
|
}
|
|
#endif // defined( _DEBUG )
|
|
|
|
if ( dirtyFlags == eDirty_None && m_bSentInitialCriteria )
|
|
{ return false; }
|
|
|
|
//
|
|
// Build update
|
|
//
|
|
|
|
bool bOverwrite = !m_bSentInitialCriteria;
|
|
if ( bOverwrite )
|
|
{ msgOptions.set_overwrite_existing( true ); }
|
|
|
|
// UI State
|
|
if ( dirtyFlags & eDirty_UIState )
|
|
{
|
|
if ( !SyncedUIStateEqual( m_lastSentUIState, m_localUIState ) )
|
|
{
|
|
msgOptions.mutable_player_uistate()->CopyFrom( m_localUIState );
|
|
m_lastSentUIState = m_localUIState;
|
|
}
|
|
else
|
|
{
|
|
dirtyFlags = ( dirtyFlags & ~eDirty_UIState );
|
|
}
|
|
}
|
|
else if ( bOverwrite )
|
|
{
|
|
m_lastSentUIState.Clear();
|
|
}
|
|
|
|
// Group Criteria
|
|
if ( dirtyFlags & eDirty_GroupCriteria )
|
|
{
|
|
if ( bOverwrite )
|
|
{
|
|
msgOptions.mutable_group_criteria()->CopyFrom( m_localGroupCriteria.Proto() );
|
|
}
|
|
else
|
|
{
|
|
// It may be that we were flagged dirty but turned out not to be if our state changed to and fro. Wipe
|
|
// dirty flag so we know at the end if we ended up with nothing.
|
|
CTFGroupMatchCriteria msgGroupCriteria;
|
|
bool bDelta = msgGroupCriteria.MakeDelta( m_lastSentGroupCriteria, m_localGroupCriteria );
|
|
if ( bDelta )
|
|
{ msgOptions.mutable_group_criteria()->CopyFrom( msgGroupCriteria.Proto() ); }
|
|
else
|
|
{ dirtyFlags = ( dirtyFlags & ~eDirty_GroupCriteria ); }
|
|
}
|
|
m_bGroupCriteriaChanged = false;
|
|
m_lastSentGroupCriteria = m_localGroupCriteria;
|
|
}
|
|
else if ( bOverwrite )
|
|
{
|
|
m_lastSentGroupCriteria = CTFGroupMatchCriteria();
|
|
}
|
|
|
|
// Player criteria
|
|
if ( dirtyFlags & eDirty_PlayerCriteria )
|
|
{
|
|
if ( bOverwrite )
|
|
{
|
|
msgOptions.mutable_player_criteria()->CopyFrom( m_localPerPlayerCriteria.Proto() );
|
|
}
|
|
else
|
|
{
|
|
// It may be that we were flagged dirty but turned out not to be if our state changed to and fro. Wipe
|
|
// dirty flag so we know at the end if we ended up with nothing.
|
|
CTFPerPlayerMatchCriteria msgPlayerCriteria;
|
|
bool bDelta = msgPlayerCriteria.MakeDelta( m_lastSentPerPlayerCriteria, m_localPerPlayerCriteria );
|
|
if ( bDelta )
|
|
{ msgOptions.mutable_player_criteria()->CopyFrom( msgPlayerCriteria.Proto() ); }
|
|
else
|
|
{ dirtyFlags = ( dirtyFlags & ~eDirty_PlayerCriteria ); }
|
|
|
|
}
|
|
m_bPerPlayerCriteriaChanged = false;
|
|
m_lastSentPerPlayerCriteria = m_localPerPlayerCriteria;
|
|
}
|
|
else if ( bOverwrite )
|
|
{
|
|
m_lastSentPerPlayerCriteria = CTFPerPlayerMatchCriteria();
|
|
}
|
|
|
|
m_bSentInitialCriteria = true;
|
|
|
|
// If our state changed to and fro we may have decided some of these flags were empty above, and thus the message
|
|
// ended up empty
|
|
return ( !m_bSentInitialCriteria || dirtyFlags != eDirty_None );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::SendPartyChat( const char *pszMessage )
|
|
{
|
|
// Check length (the UI should probably be enforcing this)
|
|
int nLen = V_strlen( pszMessage ) + 1;
|
|
if ( nLen > MAX_PARTY_CHAT_MSG )
|
|
{
|
|
Assert( !"The UI should really be enforcing max party message length" );
|
|
return;
|
|
}
|
|
|
|
// If we are not in a situation where anyone can hear us, just bounce the chat around locally
|
|
if ( !BHaveActiveParty() )
|
|
{
|
|
PostChatGameEvent( steamapicontext->SteamUser()->GetSteamID(), k_eTFPartyChatType_MemberChat, pszMessage );
|
|
return;
|
|
}
|
|
|
|
bool bSent = false;
|
|
CProtoBufMsg<CMsgPartySendChat> msg( k_EMsgGCParty_SendChat );
|
|
msg.Body().set_party_id( m_unActivePartyID );
|
|
msg.Body().set_msg( pszMessage );
|
|
|
|
// Message is likely to be lost if not healthy
|
|
if ( GTFGCClientSystem()->BHealthyGCConnection() )
|
|
{ bSent = GCClientSystem()->BSendMessage( msg ); }
|
|
|
|
if ( !bSent )
|
|
{
|
|
PartyWarn( "Failed to send party chat:\n%s\n", msg.Body().DebugString().c_str() );
|
|
PostChatGameEvent( steamapicontext->SteamUser()->GetSteamID(), k_eTFPartyChatType_Synthetic_SendFailed,
|
|
pszMessage );
|
|
return;
|
|
}
|
|
|
|
PartyDbg( "Sent party chat:\n%s\n", msg.Body().DebugString().c_str() );
|
|
return;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::ReceivedPartyChatMsg( CMsgPartyChatMsg &msg )
|
|
{
|
|
ETFPartyChatType eType = msg.type();
|
|
CSteamID steamIDActor = CSteamID( msg.actor_id() );
|
|
auto &strMsg = msg.msg();
|
|
|
|
if ( msg.has_actor_id() && ( !steamIDActor.IsValid() ||
|
|
!steamIDActor.BIndividualAccount() ||
|
|
steamIDActor.GetEUniverse() != GetUniverse() ) )
|
|
{
|
|
Assert( !"Bogus party chat actor (corrupt?)" );
|
|
PartyWarn( "Received party chat message with invalid actor, type %d, actor %s: \"%s\"\n",
|
|
eType, steamIDActor.Render(), strMsg.c_str() );
|
|
return;
|
|
}
|
|
|
|
if ( !ETFPartyChatType_IsValid( eType ) )
|
|
{
|
|
Assert( !"Unhandled party chat message type" );
|
|
PartyWarn( "Received party chat message with unknown type %d, actor %s: \"%s\"\n",
|
|
eType, steamIDActor.Render(), strMsg.c_str() );
|
|
return;
|
|
}
|
|
|
|
PostChatGameEvent( steamIDActor, eType, strMsg.c_str() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::PostChatGameEvent( const CSteamID &steamIDPoster,
|
|
ETFPartyChatType eType,
|
|
const char *pszText )
|
|
{
|
|
IGameEvent *pEvent = gameeventmanager->CreateEvent( "party_chat" );
|
|
if ( !pEvent )
|
|
{ return; }
|
|
pEvent->SetString( "steamid", CFmtStr("%llu", steamIDPoster.ConvertToUint64() ) );
|
|
pEvent->SetString( "text", pszText );
|
|
pEvent->SetInt( "type", eType );
|
|
gameeventmanager->FireEventClientSide( pEvent );
|
|
|
|
PartyDbg( "[Chat] %s [%s (%d)]: %s\n",
|
|
steamIDPoster.Render(), ETFPartyChatType_Name( eType ).c_str(), eType, pszText );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnSetOptionsReply( const CProtoBufMsg< CMsgPartySetOptions > &msg,
|
|
const CProtoBufMsg< CMsgPartySetOptionsResponse > &reply )
|
|
{
|
|
NoteUnused( msg );
|
|
NoteUnused( reply );
|
|
Assert( m_bPendingReliableCriteriaMsg );
|
|
m_bPendingReliableCriteriaMsg = false;
|
|
m_unPendingReliableCriteriaMsgParty = 0u;
|
|
|
|
// TODO(Universal Parties): In debug, Diff Party object with known options
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnQueueForMatchReply( const CProtoBufMsg< CMsgPartyQueueForMatch > &msg,
|
|
const CProtoBufMsg< CMsgPartyQueueForMatchResponse > &reply )
|
|
{
|
|
ETFMatchGroup eMatchGroup = msg.Body().match_group();
|
|
Assert( BHavePendingQueueMsg( eMatchGroup ) );
|
|
NoteUnused( reply );
|
|
SetPendingQueueMsg( eMatchGroup, false );
|
|
// Because we predict queue state, we need to double-check that we actually ended up in queue and fire the right
|
|
// events etc if not. If queuing succeeded, this should be a no-op
|
|
UpdateActiveParty();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnQueueForStandbyReply( const CProtoBufMsg< CMsgPartyQueueForStandby > &msg,
|
|
const CProtoBufMsg< CMsgPartyQueueForStandbyResponse > &reply )
|
|
{
|
|
NoteUnused( msg );
|
|
NoteUnused( reply );
|
|
Assert( m_bPendingStandbyQueueMsg );
|
|
m_bPendingStandbyQueueMsg = false;
|
|
// Because we predict queue state, we need to double-check that we actually ended up in queue and fire the right
|
|
// events etc if not. If queuing succeeded, this should be a no-op
|
|
UpdateActiveParty();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnRemoveFromQueueReply( const CProtoBufMsg< CMsgPartyRemoveFromQueue > &msg,
|
|
const CProtoBufMsg< CMsgPartyRemoveFromQueueResponse > &reply )
|
|
{
|
|
NoteUnused( reply );
|
|
ETFMatchGroup eMatchGroup = msg.Body().match_group();
|
|
Assert( BHavePendingQueueCancelMsg( eMatchGroup ) );
|
|
SetPendingQueueCancelMsg( eMatchGroup, false );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnRemoveFromStandbyQueueReply( const CProtoBufMsg< CMsgPartyRemoveFromStandbyQueue > &msg,
|
|
const CProtoBufMsg< CMsgPartyRemoveFromStandbyQueueResponse > &reply )
|
|
{
|
|
NoteUnused( msg );
|
|
NoteUnused( reply );
|
|
Assert( m_bPendingStandbyQueueCancelMsg );
|
|
m_bPendingStandbyQueueCancelMsg = false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::LeaveActiveParty()
|
|
{
|
|
// TODO(Universal Parties): Fixup GC side ExitMatchmaking message and give us one
|
|
if ( !m_unActivePartyID )
|
|
{ return; }
|
|
|
|
PartyMsg( "Sending request to leave party %llu\n", m_unActivePartyID );
|
|
CProtoBufMsg<CMsgExitMatchmaking> msg( k_EMsgGCExitMatchmaking );
|
|
msg.Body().set_party_id( m_unActivePartyID );
|
|
GCClientSystem()->BSendMessage( msg );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPartyClient::GetNumIncomingInvites() const
|
|
{
|
|
return m_vecIncomingInvites.Count();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CTFPartyInvite *CTFPartyClient::GetIncomingInvite( int i ) const
|
|
{
|
|
int nCount = m_vecIncomingInvites.Count();
|
|
Assert( i >= 0 && i < nCount );
|
|
if ( i < 0 || i >= nCount )
|
|
{ return nullptr; }
|
|
|
|
return m_vecIncomingInvites[ i ].pInvite;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BHaveIncomingInviteForSteamID( CSteamID steamID ) const
|
|
{
|
|
int nNumIncomingInvites = GetNumIncomingInvites();
|
|
for ( int i = 0; i < nNumIncomingInvites; i++ )
|
|
{
|
|
if ( GetIncomingInvite( i )->GetInviter() == steamID )
|
|
{ return true; }
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPartyClient::GetNumOutgoingInvites() const
|
|
{
|
|
return m_vecOutgoingInvites.Count();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CSteamID CTFPartyClient::GetOutgoingInvite( int i ) const
|
|
{
|
|
int nCount = m_vecOutgoingInvites.Count();
|
|
Assert( i >= 0 && i < nCount );
|
|
if ( i < 0 || i >= nCount )
|
|
{ return CSteamID(); }
|
|
|
|
return m_vecOutgoingInvites[ i ];
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BHaveOutgoingInviteForSteamID( CSteamID steamID ) const
|
|
{
|
|
int nNumOutgoingInvites = GetNumOutgoingInvites();
|
|
for ( int i = 0; i < nNumOutgoingInvites; i++ )
|
|
{
|
|
if ( GetOutgoingInvite( i ) == steamID )
|
|
{ return true; }
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPartyClient::GetNumOutgoingJoinRequests() const
|
|
{
|
|
return m_vecOutgoingJoinRequests.Count();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CTFPartyInvite *CTFPartyClient::GetOutgoingJoinRequest( int i ) const
|
|
{
|
|
int nCount = m_vecOutgoingJoinRequests.Count();
|
|
Assert( i >= 0 && i < nCount );
|
|
if ( i < 0 || i >= nCount )
|
|
{ return nullptr; }
|
|
|
|
return m_vecOutgoingJoinRequests[ i ].pInvite;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BHaveOutgoingJoinRequestForSteamID( CSteamID steamID ) const
|
|
{
|
|
int nNumOutgoingJoinRequests = GetNumOutgoingJoinRequests();
|
|
for ( int i = 0; i < nNumOutgoingJoinRequests; i++ )
|
|
{
|
|
if ( GetOutgoingJoinRequest( i )->GetInviter() == steamID )
|
|
{ return true; }
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
int CTFPartyClient::GetNumIncomingJoinRequests() const
|
|
{
|
|
return m_vecIncomingJoinRequests.Count();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CSteamID CTFPartyClient::GetIncomingJoinRequest( int i ) const
|
|
{
|
|
int nCount = m_vecIncomingJoinRequests.Count();
|
|
Assert( i >= 0 && i < nCount );
|
|
if ( i < 0 || i >= nCount )
|
|
{ return CSteamID(); }
|
|
|
|
return m_vecIncomingJoinRequests[ i ];
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::BHaveIncomingJoinRequestForSteamID( CSteamID steamID ) const
|
|
{
|
|
int nNumIncomingJoinRequests = GetNumIncomingJoinRequests();
|
|
for ( int i = 0; i < nNumIncomingJoinRequests; i++ )
|
|
{
|
|
if ( GetIncomingJoinRequest( i ) == steamID )
|
|
{ return true; }
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::UpdateIncomingInvitesOrOutgoingJoinRequests()
|
|
{
|
|
CUtlVector< PartyInvite_t > vecNewIncomingInvites;
|
|
CUtlVector< PartyInvite_t > vecNewOutgoingJoinRequests;
|
|
bool bIncomingInvitesChanged = false;
|
|
bool bOutgoingJoinRequestsChanged = false;
|
|
bool bIgnoreInvites = GetIgnorePartyInvites();
|
|
|
|
// Invites we're going to silently drop
|
|
CUtlVector< CSteamID > vecIgnoredInvites;
|
|
|
|
if ( m_pSOCache )
|
|
{
|
|
CSharedObjectTypeCache *pTypeCache = m_pSOCache->FindBaseTypeCache( CTFPartyInvite::k_nTypeID );
|
|
if ( pTypeCache )
|
|
{
|
|
int nCount = pTypeCache->GetCount();
|
|
for ( int i = 0; i < nCount; i++ )
|
|
{
|
|
CTFPartyInvite *pInvite = static_cast< CTFPartyInvite * >( pTypeCache->GetObject( i ) );
|
|
CSteamID steamIDInviter = pInvite->GetInviter();
|
|
auto fnFindOldInvite = [steamIDInviter](const PartyInvite_t &oldInvite) -> bool \
|
|
{
|
|
// The old CTFPartyInvite pointer is dead here!
|
|
return oldInvite.steamIDInviter == steamIDInviter;
|
|
};
|
|
|
|
switch ( pInvite->GetType() )
|
|
{
|
|
case CTFParty::ePending_Invite:
|
|
{
|
|
if ( bIgnoreInvites || !BAllowedToPartyWith( steamIDInviter ) )
|
|
{
|
|
// Will nuke below
|
|
vecIgnoredInvites.AddToTail( pInvite->GetInviter() );
|
|
}
|
|
else
|
|
{
|
|
int idxOld = m_vecIncomingInvites.FindPredicate( fnFindOldInvite );
|
|
if ( idxOld == m_vecIncomingInvites.InvalidIndex() )
|
|
{ bIncomingInvitesChanged = true; }
|
|
vecNewIncomingInvites.AddToTail( PartyInvite_t( { pInvite, steamIDInviter } ) );
|
|
}
|
|
}
|
|
break;
|
|
case CTFParty::ePending_JoinRequest:
|
|
{
|
|
int idxOld = m_vecOutgoingJoinRequests.FindPredicate( fnFindOldInvite );
|
|
if ( idxOld == m_vecOutgoingJoinRequests.InvalidIndex() )
|
|
{ bOutgoingJoinRequestsChanged = true; }
|
|
vecNewOutgoingJoinRequests.AddToTail( PartyInvite_t( { pInvite, steamIDInviter } ) );
|
|
}
|
|
break;
|
|
default:
|
|
Assert( !"Unhandled enum value" );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Any invites we should not present to user/UI?
|
|
FOR_EACH_VEC( vecIgnoredInvites, idxIgnored )
|
|
{
|
|
PartyDbg( "Auto-rejecting invite from %s - ignore party invites [%d], allowed to party with [%d]\n",
|
|
vecIgnoredInvites[idxIgnored].Render(), bIgnoreInvites,
|
|
BAllowedToPartyWith( vecIgnoredInvites[idxIgnored] ) );
|
|
CancelOutgoingJoinRequestOrIncomingInvite( vecIgnoredInvites[idxIgnored] );
|
|
}
|
|
|
|
// If we didn't find any new objects, the lists changed only if there were more previously
|
|
if ( !bIncomingInvitesChanged && vecNewIncomingInvites.Count() != m_vecIncomingInvites.Count() )
|
|
{ bIncomingInvitesChanged = true; }
|
|
|
|
if ( !bOutgoingJoinRequestsChanged && vecNewOutgoingJoinRequests.Count() != m_vecOutgoingJoinRequests.Count() )
|
|
{ bOutgoingJoinRequestsChanged = true; }
|
|
|
|
if ( bIncomingInvitesChanged )
|
|
{
|
|
m_vecIncomingInvites = vecNewIncomingInvites;
|
|
OnIncomingInvitesChanged();
|
|
}
|
|
|
|
if ( bOutgoingJoinRequestsChanged )
|
|
{
|
|
m_vecOutgoingJoinRequests = vecNewOutgoingJoinRequests;
|
|
OnOutgoingJoinRequestsChanged();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
bool CTFPartyClient::UpdateActiveParty()
|
|
{
|
|
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ );
|
|
CTFParty *pActiveParty = nullptr;
|
|
if ( m_pSOCache )
|
|
{
|
|
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() );
|
|
pActiveParty = static_cast<CTFParty*>( pTypeCache->GetObject( pTypeCache->GetCount() - 1 ) );
|
|
}
|
|
}
|
|
|
|
CSteamID steamIDLocal = steamapicontext->SteamUser()->GetSteamID();
|
|
int idxLocalPlayer = pActiveParty ? pActiveParty->GetMemberIndexBySteamID( steamIDLocal ) : -1;
|
|
|
|
// Comparing queue state
|
|
//
|
|
// Once we have a pending message, tell consumers we are in queue. We keep reporting in queue even if we have a
|
|
// pending cancel message - since we accept inbound matches during that race period.
|
|
//
|
|
// TODO: Once we have an accept-reject-match step we could predict queue exit as well, and auto-reject incoming
|
|
// matches in the short window.
|
|
CUtlMap< ETFMatchGroup, bool > mapChangedQueues( DefLessFunc( ETFMatchGroup ) );
|
|
for ( int iMatchGroup = k_eTFMatchGroup_First; iMatchGroup < ETFMatchGroup_ARRAYSIZE; iMatchGroup++ )
|
|
{
|
|
if ( !ETFMatchGroup_IsValid( iMatchGroup ) )
|
|
{ continue; }
|
|
ETFMatchGroup eMatchGroup = (ETFMatchGroup)iMatchGroup;
|
|
bool bWasQueued = BInQueueForMatchGroup( eMatchGroup );
|
|
bool bNowQueued = ( BHavePendingQueueMsg( eMatchGroup ) ||
|
|
( pActiveParty && pActiveParty->BQueuedForMatchGroup( eMatchGroup ) ) );
|
|
if ( bWasQueued != bNowQueued )
|
|
{ mapChangedQueues.Insert( eMatchGroup, bNowQueued ); }
|
|
}
|
|
|
|
// Standby queue state
|
|
bool bWasQueuedStandby = m_bInStandbyQueue;
|
|
bool bNowQueuedStandby = idxLocalPlayer != -1 ? pActiveParty->BMemberRequestingLobbyStandby( idxLocalPlayer ) : false;
|
|
bool bQueueStandbyChanged = ( bWasQueuedStandby != bNowQueuedStandby );
|
|
|
|
bool bPartyChanged = ( pActiveParty != m_pActiveParty );
|
|
|
|
// Compare leaders
|
|
Assert( steamapicontext );
|
|
Assert( steamapicontext->SteamUser() );
|
|
bool bIsLeader = pActiveParty ? pActiveParty->GetLeader() == steamIDLocal : true;
|
|
bool bIsLeaderChanged = ( bIsLeader != m_bIsActivePartyLeader );
|
|
|
|
// Compare criteria
|
|
bool bWasUsingActiveCriteria = BEffectiveGroupCriteriaFromParty();
|
|
auto &newCriteria = pActiveParty ? pActiveParty->Obj().group_criteria() \
|
|
: CTFGroupMatchCriteriaProto::default_instance();
|
|
bool bActiveCriteriaChanged = CTFGroupMatchCriteria().MakeDelta( m_activePartyCriteria,
|
|
ConstRefTFGroupMatchCriteria( newCriteria ) );
|
|
|
|
// Compare party members
|
|
CUtlVectorFixed< Member_t, MAX_PARTY_SIZE > vecNewMembers;
|
|
CUtlVectorFixed< CSteamID, MAX_PARTY_SIZE > vecAddedMembers;
|
|
CUtlVectorFixed< CSteamID, MAX_PARTY_SIZE > vecRemovedMembers;
|
|
CUtlVectorFixed< Member_t, MAX_PARTY_SIZE > vecMembersChangedOnlineState;
|
|
if ( pActiveParty )
|
|
{
|
|
for ( int idx = 0; idx < pActiveParty->GetNumMembers(); idx++ )
|
|
{
|
|
bool bOnline = pActiveParty->GetMemberActivity( idx ).online();
|
|
vecNewMembers.AddToTail( Member_t{ pActiveParty->GetMember( idx ), bOnline } );
|
|
}
|
|
vecNewMembers.SortPredicate( []( const Member_t &a, const Member_t &b ) {
|
|
return a.steamID.GetAccountID() < b.steamID.GetAccountID();
|
|
} );
|
|
}
|
|
else
|
|
{
|
|
// Alll by mysellllf
|
|
vecNewMembers.AddToTail( Member_t{ steamapicontext->SteamUser()->GetSteamID(), /* bOnline */ true } );
|
|
}
|
|
|
|
int idxNew = 0;
|
|
int idxOld = 0;
|
|
while ( idxNew < vecNewMembers.Count() || idxOld < m_activePartyMembers.Count() )
|
|
{
|
|
auto newID = ( idxNew < vecNewMembers.Count() ) ? vecNewMembers[idxNew].steamID.GetAccountID() : 0;
|
|
auto oldID = ( idxOld < m_activePartyMembers.Count() ) ? m_activePartyMembers[idxOld].steamID.GetAccountID() : 0;
|
|
|
|
if ( oldID && ( !newID || oldID < newID ) )
|
|
{
|
|
// Delete idxOld
|
|
vecRemovedMembers.AddToTail( m_activePartyMembers[idxOld].steamID );
|
|
idxOld++;
|
|
}
|
|
else if ( newID && ( !oldID || newID < oldID ) )
|
|
{
|
|
// Add idxNew
|
|
vecAddedMembers.AddToTail( vecNewMembers[idxNew].steamID );
|
|
idxNew++;
|
|
}
|
|
else
|
|
{
|
|
// Same, online state different?
|
|
if ( m_activePartyMembers[idxOld].bOnline != vecNewMembers[idxNew].bOnline )
|
|
{ vecMembersChangedOnlineState.AddToTail( vecNewMembers[idxNew] ); }
|
|
|
|
Assert( newID == oldID && oldID != 0 && newID != 0 );
|
|
idxNew++;
|
|
idxOld++;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Compare pending players
|
|
//
|
|
|
|
// See if we are auto-acting on any
|
|
EPartyJoinRequestMode eMode = GetPartyJoinRequestMode();
|
|
// In request to join mode, we take no automatic actions.
|
|
bool bAutoAccept = ( eMode == k_ePartyJoinRequestMode_OpenToFriends );
|
|
bool bAutoReject = ( eMode == k_ePartyJoinRequestMode_ClosedToFriends );
|
|
CUtlVector< CSteamID > vecAutoAcceptJoinRequests;
|
|
CUtlVector< CSteamID > vecAutoRejectJoinRequests;
|
|
|
|
bool bIncomingJoinRequestsChanged = false;
|
|
bool bOutgoingInvitesChanged = false;
|
|
CUtlVector< CSteamID > vecNewIncomingJoinRequests;
|
|
CUtlVector< CSteamID > vecNewOutgoingInvites;
|
|
int nNewPendingPlayers = pActiveParty ? pActiveParty->GetNumPendingPlayers() : 0;
|
|
for ( int i = 0; i < nNewPendingPlayers; i++ )
|
|
{
|
|
CSteamID steamID = pActiveParty->GetPendingPlayer( i );
|
|
switch ( pActiveParty->GetPendingPlayerType( i ) )
|
|
{
|
|
case CTFParty::ePending_Invite:
|
|
{
|
|
if ( m_vecOutgoingInvites.Find( steamID ) == m_vecOutgoingInvites.InvalidIndex() )
|
|
{ bOutgoingInvitesChanged = true; }
|
|
vecNewOutgoingInvites.AddToTail( steamID );
|
|
}
|
|
break;
|
|
case CTFParty::ePending_JoinRequest:
|
|
{
|
|
bool bHandled = false;
|
|
// Are we going to auto-handle this?
|
|
if ( BControllingPartyActions() )
|
|
{
|
|
if ( bAutoReject || !BAllowedToPartyWith( steamID ) )
|
|
{
|
|
vecAutoRejectJoinRequests.AddToTail( steamID );
|
|
bHandled = true;
|
|
}
|
|
else if ( bAutoAccept )
|
|
{
|
|
vecAutoAcceptJoinRequests.AddToTail( steamID );
|
|
bHandled = true;
|
|
}
|
|
}
|
|
|
|
if ( !bHandled )
|
|
{
|
|
if ( m_vecIncomingJoinRequests.Find( steamID ) == m_vecIncomingJoinRequests.InvalidIndex() )
|
|
{ bIncomingJoinRequestsChanged = true; }
|
|
vecNewIncomingJoinRequests.AddToTail( steamID );
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
Assert( !"Unhandled enum value" );
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If we didn't find anyone new, then we only changed if there were more previously
|
|
if ( !bOutgoingInvitesChanged && m_vecOutgoingInvites.Count() != vecNewOutgoingInvites.Count() )
|
|
{ bOutgoingInvitesChanged = true; }
|
|
if ( !bIncomingJoinRequestsChanged && m_vecIncomingJoinRequests.Count() != vecNewIncomingJoinRequests.Count() )
|
|
{ bIncomingJoinRequestsChanged = true; }
|
|
|
|
//
|
|
// Change state
|
|
//
|
|
|
|
if ( bIsLeaderChanged )
|
|
{
|
|
m_bIsActivePartyLeader = bIsLeader;
|
|
}
|
|
|
|
if ( vecAddedMembers.Count() || vecRemovedMembers.Count() || vecMembersChangedOnlineState.Count() )
|
|
{ m_activePartyMembers = vecNewMembers; }
|
|
|
|
if ( bActiveCriteriaChanged )
|
|
{ m_activePartyCriteria = newCriteria; }
|
|
|
|
if ( bPartyChanged )
|
|
{
|
|
m_pActiveParty = pActiveParty;
|
|
m_unActivePartyID = pActiveParty ? pActiveParty->GetGroupID() : 0u;
|
|
}
|
|
|
|
FOR_EACH_MAP_FAST( mapChangedQueues, idx )
|
|
{ InternalSetInQueueForMatchGroup( mapChangedQueues.Key( idx ), mapChangedQueues[idx] ); }
|
|
|
|
if ( bQueueStandbyChanged )
|
|
{ m_bInStandbyQueue = bNowQueuedStandby; }
|
|
|
|
if ( bOutgoingInvitesChanged )
|
|
{ m_vecOutgoingInvites = vecNewOutgoingInvites; }
|
|
|
|
if ( bIncomingJoinRequestsChanged )
|
|
{ m_vecIncomingJoinRequests = vecNewIncomingJoinRequests; }
|
|
|
|
//
|
|
// Perform any auto-actions
|
|
// Do this after changing state because some of these calls report up our active party ID and such.
|
|
//
|
|
FOR_EACH_VEC( vecAutoAcceptJoinRequests, idx )
|
|
{
|
|
CSteamID steamID = vecAutoAcceptJoinRequests[idx];
|
|
PartyMsg( "Auto-accepting join request from %s - autoaccept [%d], can party with [%d]\n",
|
|
steamID.Render(), bAutoAccept, BAllowedToPartyWith( steamID ) );
|
|
// Bypass the explicit invite path and just auto-accept. The normal path will be confused that it doesn't see
|
|
// the expected join request, because we bypassed creating one. We already checked that we're allowed to party
|
|
// with this player above when considering if it was a valid auto-accept candidate.
|
|
bool bInvited = BSendPartyInvitePlayerMessageInternal( steamID, true );
|
|
if ( !bInvited )
|
|
{ PartyWarn( "Failed to auto-invite player %s\n", steamID.Render() ); }
|
|
}
|
|
|
|
FOR_EACH_VEC( vecAutoRejectJoinRequests, idx )
|
|
{
|
|
CSteamID steamID = vecAutoRejectJoinRequests[idx];
|
|
PartyMsg( "Auto-rejecting join request from %s - autoreject [%d], can party with [%d]\n",
|
|
steamID.Render(), bAutoReject, BAllowedToPartyWith( steamID ) );
|
|
CancelOutgoingInviteOrIncomingJoinRequest( steamID );
|
|
}
|
|
|
|
//
|
|
// Fire state changes
|
|
//
|
|
|
|
// OnNewParty
|
|
if ( bPartyChanged )
|
|
{ OnNewParty(); }
|
|
|
|
// OnPartyMemberOnlineStateChanged
|
|
FOR_EACH_VEC( vecMembersChangedOnlineState, idx )
|
|
{ OnPartyMemberOnlineStateChanged( vecMembersChangedOnlineState[idx] ); }
|
|
|
|
// OnPartyMemberGained
|
|
FOR_EACH_VEC( vecAddedMembers, idx )
|
|
{ OnPartyMemberGained( vecAddedMembers[idx] ); }
|
|
|
|
// OnPartyMemberLost
|
|
FOR_EACH_VEC( vecRemovedMembers, idx )
|
|
{ OnPartyMemberLost( vecRemovedMembers[idx] ); }
|
|
|
|
// OnIsLeaderChanged
|
|
if ( bIsLeaderChanged )
|
|
{ OnIsLeaderChanged( bIsLeader ); }
|
|
|
|
// OnActiveCriteriaChanged
|
|
// if it changed above or if we switched the source of active criteria from above state changes
|
|
bool bNowUsingActive = BEffectiveGroupCriteriaFromParty();
|
|
if ( bActiveCriteriaChanged || ( bNowUsingActive != bWasUsingActiveCriteria ) )
|
|
{ OnEffectiveCriteriaChanged(); }
|
|
|
|
// OnQueueChanged
|
|
FOR_EACH_MAP_FAST( mapChangedQueues, idx )
|
|
{ OnInQueueChanged( mapChangedQueues.Key( idx ) ); }
|
|
|
|
// OnStandbyQueueChanged
|
|
if ( bQueueStandbyChanged )
|
|
{ OnInStandbyQueueChanged(); }
|
|
|
|
// OnOutgoingInvitesChanged
|
|
if ( bOutgoingInvitesChanged )
|
|
{ OnOutgoingInvitesChanged(); }
|
|
|
|
// OnIncomingJoinRequestsChanged
|
|
if ( bIncomingJoinRequestsChanged )
|
|
{ OnIncomingJoinRequestsChanged(); }
|
|
|
|
// TODO(Universal Parties): Check for unexpected queue state change, fire better coarse events
|
|
OnPartyUpdated();
|
|
|
|
// Trigger steam rich presence update -- many sub-changes might trigger these, so having the callback functions each
|
|
// request one would fire it many times on e.g. joining another party.
|
|
// Steam rich presence includes:
|
|
// - queue state
|
|
// - Party ID
|
|
// - Num *online* party members
|
|
if ( bPartyChanged || bQueueStandbyChanged || mapChangedQueues.Count() ||
|
|
vecAddedMembers.Count() || vecRemovedMembers.Count() || vecMembersChangedOnlineState.Count() )
|
|
{
|
|
GetClientModeTFNormal()->UpdateSteamRichPresence();
|
|
}
|
|
|
|
return bPartyChanged;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::CheckResetSentOptions()
|
|
{
|
|
PartyDbg( "Resetting sent criteria" );
|
|
// We still want to track how long we've been dirty I think, e.g. party leader changes and we were already dirty -
|
|
// this is a change that dirties us like any other.
|
|
//
|
|
// m_flPendingChangesTime = -1.f;
|
|
|
|
auto *pParty = GetActiveParty();
|
|
if ( pParty && !BHaveAnyPendingQueueMsg() &&
|
|
( !m_bPendingReliableCriteriaMsg || m_unActivePartyID != m_unPendingReliableCriteriaMsgParty ) )
|
|
{
|
|
// If we have a party and don't have pending messages that adjust options, we can safely guess at what our 'last
|
|
// sent' values should be.
|
|
m_lastSentUIState = ( this->BIsPartyLeader() ? pParty->Obj().leader_ui_state() : TFSyncedMMUIState() );
|
|
m_lastSentGroupCriteria = pParty->GetGroupMatchCriteria();
|
|
int idx = pParty->GetMemberIndexBySteamID( steamapicontext->SteamUser()->GetSteamID() );
|
|
if ( idx != -1 )
|
|
{ m_lastSentPerPlayerCriteria = pParty->GetMemberMatchCriteria( idx ); }
|
|
else
|
|
{ m_lastSentPerPlayerCriteria.Clear(); }
|
|
}
|
|
else
|
|
{
|
|
// If we don't have a party or pending messages are making this a sync-nightmare to calculate, just reset all
|
|
// state and force a fresh send. This should be the un-common case so the extra message load is worth the
|
|
// complexity tradeoff.
|
|
if ( m_unActivePartyID )
|
|
{
|
|
PartyWarn( "Re-joining party %llu we previously had queued messages for. "
|
|
"Triggering full resend of options.\n", m_unActivePartyID );
|
|
}
|
|
// Set all last-sents to the default values. Next CheckSendUpdates will set the overwrite flag due to
|
|
// !m_bSentInitialCriteria, and then we'll diff against defaults for what to put in it.
|
|
m_lastSentUIState.Clear();
|
|
m_lastSentGroupCriteria.Clear();
|
|
m_lastSentPerPlayerCriteria.Clear();
|
|
// Sending an initial update will overwrite all settings, retroactively making our 'last sent was all defaults' the
|
|
// truth.
|
|
m_bSentInitialCriteria = false;
|
|
|
|
}
|
|
|
|
// Maybe not actually changed from default, but we want CheckSendUpdates to diff them and see.
|
|
m_bGroupCriteriaChanged = true;
|
|
m_bPerPlayerCriteriaChanged = true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnOutgoingInvitesChanged()
|
|
{
|
|
PartyDbg( "Outgoing party invites updated [%d]\n", GetNumOutgoingInvites() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnIncomingJoinRequestsChanged()
|
|
{
|
|
PartyDbg( "Incoming party join requests updated [%d]\n", GetNumIncomingJoinRequests() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnNewParty()
|
|
{
|
|
PartyMsg( "Joining party %llu\n", GetActivePartyID() );
|
|
CheckResetSentOptions();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnPartyMemberOnlineStateChanged( const Member_t &member )
|
|
{
|
|
PartyMsg( "Member %s now %s\n", member.steamID.Render(), member.bOnline ? "online" : "offline" );
|
|
ETFPartyChatType eChatType = ( member.bOnline
|
|
? k_eTFPartyChatType_Synthetic_MemberOnline
|
|
: k_eTFPartyChatType_Synthetic_MemberOffline );
|
|
|
|
PostChatGameEvent( member.steamID, eChatType, nullptr );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnPartyMemberGained( CSteamID steamID )
|
|
{
|
|
PostChatGameEvent( steamID, k_eTFPartyChatType_Synthetic_MemberJoin, nullptr );
|
|
|
|
IGameEvent *pEvent = gameeventmanager->CreateEvent( "party_member_join" );
|
|
if ( !pEvent )
|
|
{ return; }
|
|
pEvent->SetString( "steamid", CFmtStr("%llu", steamID.ConvertToUint64() ) );
|
|
gameeventmanager->FireEventClientSide( pEvent );
|
|
|
|
PartyDbg( "Gained member %s\n", steamID.Render() );
|
|
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnPartyMemberLost( CSteamID steamID )
|
|
{
|
|
PostChatGameEvent( steamID, k_eTFPartyChatType_Synthetic_MemberLeave, nullptr );
|
|
|
|
IGameEvent *pEvent = gameeventmanager->CreateEvent( "party_member_leave" );
|
|
if ( !pEvent )
|
|
{ return; }
|
|
pEvent->SetString( "steamid", CFmtStr("%llu", steamID.ConvertToUint64() ) );
|
|
gameeventmanager->FireEventClientSide( pEvent );
|
|
|
|
PartyDbg( "Lost member %s\n", steamID.Render() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnIsLeaderChanged( bool bIsLeader )
|
|
{
|
|
auto nPID = GetActivePartyID();
|
|
if ( nPID )
|
|
{
|
|
PartyMsg( "%s leader of party %llu\n", bIsLeader ? "Became" : "No longer", nPID );
|
|
}
|
|
|
|
AssertMsg( nPID || bIsLeader, "We should be considered a leader if we have no party object" );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnEffectiveCriteriaChanged()
|
|
{
|
|
m_bQueuedEffectiveCriteriaChangeEvent = false;
|
|
IGameEvent *event = gameeventmanager ? gameeventmanager->CreateEvent( "party_criteria_changed" ) : nullptr;
|
|
if ( event )
|
|
{ gameeventmanager->FireEventClientSide( event ); }
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnInQueueChanged( ETFMatchGroup eMatchGroup )
|
|
{
|
|
if ( BInQueueForMatchGroup( eMatchGroup ) )
|
|
{ PartyMsg( "Entering queue for match group %s\n", GetMatchGroupName( eMatchGroup ) ); }
|
|
else
|
|
{ PartyMsg( "Leaving queue for match group %s\n", GetMatchGroupName( eMatchGroup ) ); }
|
|
|
|
IGameEvent *event = gameeventmanager ? gameeventmanager->CreateEvent( "party_queue_state_changed" ) : nullptr;
|
|
if ( event )
|
|
{
|
|
event->SetInt( "matchgroup", eMatchGroup );
|
|
gameeventmanager->FireEventClientSide( event );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnInStandbyQueueChanged()
|
|
{
|
|
if ( BInStandbyQueue() )
|
|
{ PartyMsg( "Entering standby queue to join party\n" ); }
|
|
else
|
|
{ PartyMsg( "Leaving standby queue\n"); }
|
|
|
|
IGameEvent *event = gameeventmanager ? gameeventmanager->CreateEvent( "party_queue_state_changed" ) : nullptr;
|
|
if ( event )
|
|
{ gameeventmanager->FireEventClientSide( event ); }
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnIncomingInvitesChanged()
|
|
{
|
|
PartyDbg( "Incoming invites updated [%d]\n", GetNumIncomingInvites() );
|
|
|
|
IGameEvent *event = gameeventmanager ? gameeventmanager->CreateEvent( "party_invites_changed" ) : nullptr;
|
|
if ( event )
|
|
{ gameeventmanager->FireEventClientSide( event ); }
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnOutgoingJoinRequestsChanged()
|
|
{
|
|
PartyDbg( "Outgoing join requests updated [%d]\n", GetNumOutgoingJoinRequests() );
|
|
|
|
IGameEvent *event = gameeventmanager ? gameeventmanager->CreateEvent( "party_invites_changed" ) : nullptr;
|
|
if ( event )
|
|
{ gameeventmanager->FireEventClientSide( event ); }
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnPartyUpdated()
|
|
{
|
|
// Fire event
|
|
IGameEvent *event = gameeventmanager ? gameeventmanager->CreateEvent( "party_updated" ) : nullptr;
|
|
if ( event )
|
|
{ gameeventmanager->FireEventClientSide( event ); }
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::OnPartyPrefChanged()
|
|
{
|
|
// Fire event
|
|
IGameEvent *event = gameeventmanager ? gameeventmanager->CreateEvent( "party_pref_changed" ) : nullptr;
|
|
if ( event )
|
|
{ gameeventmanager->FireEventClientSide( event ); }
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::SOCacheSubscribed( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent )
|
|
{
|
|
if ( steamIDOwner == ClientSteamContext().GetLocalPlayerSteamID() )
|
|
{
|
|
// Assert( m_pSOCache == nullptr ); // we *can* get two SOCacheSubscribed calls in a row.
|
|
m_pSOCache = GCClientSystem()->GetSOCache( steamIDOwner );
|
|
Assert( m_pSOCache != nullptr );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::SOCacheUnsubscribed( const CSteamID & steamIDOwner, GCSDK::ESOCacheEvent eEvent )
|
|
{
|
|
if ( steamIDOwner == ClientSteamContext().GetLocalPlayerSteamID() )
|
|
{
|
|
m_pSOCache = nullptr;
|
|
UpdateIncomingInvitesOrOutgoingJoinRequests();
|
|
UpdateActiveParty();
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::SOCreated( const CSteamID & steamIDOwner,
|
|
const GCSDK::CSharedObject *pObject,
|
|
GCSDK::ESOCacheEvent eEvent )
|
|
{
|
|
if ( pObject->GetTypeID() == CTFPartyInvite::k_nTypeID )
|
|
{
|
|
UpdateIncomingInvitesOrOutgoingJoinRequests();
|
|
return;
|
|
}
|
|
if ( pObject->GetTypeID() != CTFParty::k_nTypeID )
|
|
{ return; }
|
|
|
|
PartyDbg( "Party object received for %llu, current party %llu\n",
|
|
ToParty( pObject )->GetGroupID(), GetActivePartyID() );
|
|
|
|
// We can get double-creates that overwrite items with SOCache magic, ensure we actually changed
|
|
bool bNewParty = UpdateActiveParty();
|
|
NoteUnused( bNewParty );
|
|
AssertMsg( !bNewParty || m_pActiveParty, "Our party changed from party -> noparty in SOCreate?" );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::SOUpdated( const CSteamID & steamIDOwner,
|
|
const GCSDK::CSharedObject *pObject,
|
|
GCSDK::ESOCacheEvent eEvent )
|
|
{
|
|
if ( pObject->GetTypeID() == CTFPartyInvite::k_nTypeID )
|
|
{
|
|
UpdateIncomingInvitesOrOutgoingJoinRequests();
|
|
return;
|
|
}
|
|
if ( pObject->GetTypeID() != CTFParty::k_nTypeID )
|
|
{ return; }
|
|
|
|
auto *pParty = ToParty( pObject );
|
|
auto *pActiveParty = GetActiveParty();
|
|
// We can get SOUpdated for objects that were never SOCreated in the case of cache subscribe, apparently
|
|
if ( pActiveParty && pParty != pActiveParty )
|
|
{
|
|
PartyWarn( "Party update received for party %llu, but our current party is %llu, forcing update\n",
|
|
pParty->GetGroupID(), GetActivePartyID() );
|
|
Assert( !"SOUpdated for a party we don't think we are in" );
|
|
}
|
|
|
|
UpdateActiveParty();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CTFPartyClient::SODestroyed( const CSteamID & steamIDOwner,
|
|
const GCSDK::CSharedObject *pObject,
|
|
GCSDK::ESOCacheEvent eEvent )
|
|
{
|
|
if ( pObject->GetTypeID() == CTFPartyInvite::k_nTypeID )
|
|
{
|
|
UpdateIncomingInvitesOrOutgoingJoinRequests();
|
|
return;
|
|
}
|
|
if ( pObject->GetTypeID() != CTFParty::k_nTypeID )
|
|
{ return; }
|
|
|
|
auto nPID = ToParty( pObject )->GetGroupID();
|
|
auto nActivePID = GetActivePartyID();
|
|
if ( nPID != nActivePID )
|
|
{
|
|
PartyWarn( "SODestroyed for party %llu, but we believe our active party is %llu\n", nPID, nActivePID );
|
|
}
|
|
|
|
bool bChanged = UpdateActiveParty();
|
|
AssertMsg( bChanged, "Received SODestroyed for a party, but we don't think it affected our active party" );
|
|
NoteUnused( bChanged );
|
|
}
|
|
|
|
//
|
|
// Inbound message jobs
|
|
//
|
|
|
|
//-----------------------------------------------------------------------------
|
|
class CGCClientJobPartyChatMsg : public GCSDK::CGCClientJob
|
|
{
|
|
public:
|
|
CGCClientJobPartyChatMsg( GCSDK::CGCClient *pGCServer ) : GCSDK::CGCClientJob( pGCServer ) { }
|
|
|
|
virtual bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
|
|
{
|
|
CProtoBufMsg<CMsgPartyChatMsg> msg( pNetPacket );
|
|
GTFPartyClient()->ReceivedPartyChatMsg( msg.Body() );
|
|
return true;
|
|
}
|
|
};
|
|
GC_REG_JOB( GCSDK::CGCClient, CGCClientJobPartyChatMsg, "CGCClientJobPartyChatMsg", k_EMsgGCParty_ChatMsg, k_EServerTypeGCClient );
|
|
|
|
//-----------------------------------------------------------------------------
|
|
class CGCClientJobPartyMMError : public GCSDK::CGCClientJob
|
|
{
|
|
public:
|
|
CGCClientJobPartyMMError( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {}
|
|
|
|
virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
|
|
{
|
|
GCSDK::CProtoBufMsg<CMsgPartyMMError> msg( pNetPacket );
|
|
CMsgPartyMMError_Type eType = msg.Body().type();
|
|
|
|
PartyWarn( "Received MM Error %s\n", ( CMsgPartyMMError_Type_IsValid( eType )
|
|
? CMsgPartyMMError_Type_Name( eType ).c_str()
|
|
: CFmtStr( "<unknown (%d)>", eType ).Get() ) );
|
|
|
|
switch ( eType )
|
|
{
|
|
case CMsgPartyMMError_Type_QUEUE_KICK_NO_PING:
|
|
TFMMIssueDialog()->Show( "#TF_MM_Queue_Error_Ping_Text", "#TF_MM_Queue_Error_Ping_URL" );
|
|
break;
|
|
case CMsgPartyMMError_Type_QUEUE_KICK_AUTH:
|
|
TFMMIssueDialog()->Show( "#TF_MM_Queue_Error_VACIssue_Text", "#TF_MM_Queue_Error_VACIssue_URL" );
|
|
break;
|
|
default:
|
|
{
|
|
AssertMsg( false, "Unhandled CMsgPartyMMError Type" );
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
GC_REG_JOB( GCSDK::CGCClient, CGCClientJobPartyMMError, "CGCClientJobPartyMMError", k_EMsgGCParty_MMError, k_EServerTypeGCClient );
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CON_COMMAND( tf_party_invite_user, "Invite the given steamid to join your party, or accept a request to do so" )
|
|
{
|
|
if ( args.ArgC() != 2 )
|
|
{
|
|
ConMsg( "Usage: tf_party_invite_user <steamid>\n" );
|
|
return;
|
|
}
|
|
|
|
CSteamID steamID = UTIL_GuessSteamIDFromFuzzyInput( args[1] );
|
|
if ( !steamID.IsValid() || steamID.GetEAccountType() != k_EAccountTypeIndividual )
|
|
{
|
|
ConWarning( "Could not parse an individual Steam ID from '%s'\n", args[1] );
|
|
return;
|
|
}
|
|
|
|
if ( !GTFPartyClient()->BCanInvitePlayer( steamID ) )
|
|
{
|
|
ConWarning( "You cannot currently invite player %s to this party\n", steamID.Render() );
|
|
return;
|
|
}
|
|
|
|
if ( !GTFPartyClient()->BInvitePlayerToParty( steamID, false ) )
|
|
{
|
|
ConWarning( "Failed to invite %s to your party\n", steamID.Render() );
|
|
return;
|
|
}
|
|
|
|
ConMsg( "Invited %s to your party\n", steamID.Render() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CON_COMMAND( tf_party_request_join_user, "Request to join the given steamid's party, or accept an invite to do so" )
|
|
{
|
|
if ( args.ArgC() != 2 && args.ArgC() != 3 )
|
|
{
|
|
ConMsg( "Usage: tf_party_request_join_user <steamid> [<expect invite (1/0)>]\n" );
|
|
return;
|
|
}
|
|
|
|
bool bExpectInvite = ( args.ArgC() > 2 && V_atoi( args[2] ) );
|
|
|
|
CSteamID steamID = UTIL_GuessSteamIDFromFuzzyInput( args[1] );
|
|
if ( !steamID.IsValid() || steamID.GetEAccountType() != k_EAccountTypeIndividual )
|
|
{
|
|
ConWarning( "Could not parse an individual Steam ID from '%s'\n", args[1] );
|
|
return;
|
|
}
|
|
|
|
if ( !GTFPartyClient()->BCanRequestToJoinPlayer( steamID ) )
|
|
{
|
|
ConWarning( "You cannot currently request to join party of player %s\n", steamID.Render() );
|
|
return;
|
|
}
|
|
|
|
if ( !GTFPartyClient()->BRequestJoinPlayer( steamID, bExpectInvite ) )
|
|
{
|
|
ConWarning( "Failed to request to join party of player %s\n", steamID.Render() );
|
|
return;
|
|
}
|
|
|
|
ConMsg( "Requested to join party of player %s\n", steamID.Render() );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CON_COMMAND( tf_party_chat, "Talk to me" )
|
|
{
|
|
if ( args.ArgC() != 2 )
|
|
{
|
|
ConMsg( "Usage: tf_party_chat <message>\n" );
|
|
return;
|
|
}
|
|
|
|
const char *pszMsg = args.Arg( 1 );
|
|
GTFPartyClient()->SendPartyChat( pszMsg );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CON_COMMAND( tf_party_leave, "Leave current party" )
|
|
{
|
|
if ( args.ArgC() != 1 )
|
|
{
|
|
ConMsg( "Usage: tf_party_leave - no args\n" );
|
|
return;
|
|
}
|
|
|
|
GTFPartyClient()->LeaveActiveParty();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CON_COMMAND( tf_party_incoming_invites_debug, "Show current party invite objects" )
|
|
{
|
|
if ( args.ArgC() != 1 )
|
|
{
|
|
ConMsg( "Usage: tf_party_incoming_invites_debug - no args\n" );
|
|
return;
|
|
}
|
|
|
|
int nCount = GTFPartyClient()->GetNumIncomingInvites();
|
|
PartyMsg( "%d incoming invites%s\n", nCount, nCount ? ":" : "" );
|
|
for ( int i = 0; i < nCount; i++ )
|
|
{
|
|
PartyMsg( "%s\n", GTFPartyClient()->GetIncomingInvite( i )->Obj().DebugString().c_str() );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CON_COMMAND( tf_party_force_update, "Force a party invite now" )
|
|
{
|
|
if ( args.ArgC() != 1 )
|
|
{
|
|
ConMsg( "Usage: tf_party_force_update - no args\n" );
|
|
return;
|
|
}
|
|
|
|
GTFPartyClient()->ForcePartyUpdate();
|
|
PartyMsg( "Forced party update\n" );
|
|
}
|
|
|
|
|
|
// These commands could screw with UI flow or be abused maybe
|
|
#if defined( STAGING_ONLY ) || defined( _DEBUG )
|
|
//-----------------------------------------------------------------------------
|
|
CON_COMMAND( tf_party_queue, "Queue the party" )
|
|
{
|
|
if ( args.ArgC() != 2 )
|
|
{
|
|
ConMsg( "Usage: tf_party_queue - <matchgroup id or enum name>\n" );
|
|
return;
|
|
}
|
|
|
|
ETFMatchGroup eMatchGroup = ETFMatchGroup_FuzzyParse( args[1] );
|
|
if ( !AssertValidMatchGroup( eMatchGroup ) )
|
|
{
|
|
ConMsg( "Failed to parse a valid match group from input\n" );
|
|
return;
|
|
}
|
|
|
|
GTFPartyClient()->RequestQueueForMatch( eMatchGroup );
|
|
if ( GTFPartyClient()->BInQueueForMatchGroup( eMatchGroup ) )
|
|
{ ConMsg( "Requested MM queue for [%s]\n", GetMatchGroupName( eMatchGroup ) ); }
|
|
else
|
|
{ ConMsg( "Queue request failed for [%s]\n", GetMatchGroupName( eMatchGroup ) ); }
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CON_COMMAND( tf_party_queue_standby, "Queue for standby for your party" )
|
|
{
|
|
if ( args.ArgC() != 1 )
|
|
{
|
|
ConMsg( "Usage: tf_party_queue_standby - no args\n" );
|
|
return;
|
|
}
|
|
|
|
GTFPartyClient()->RequestQueueForStandby();
|
|
if ( GTFPartyClient()->BInStandbyQueue() )
|
|
{ ConMsg("Requested queue for standby\n"); }
|
|
else
|
|
{ ConMsg("Queue request failed\n"); }
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CON_COMMAND( tf_party_queue_cancel, "Un-queue the party" )
|
|
{
|
|
if ( args.ArgC() != 2 )
|
|
{
|
|
ConMsg( "Usage: tf_party_queue_cancel - <matchgroup id or enum name>\n" );
|
|
return;
|
|
}
|
|
|
|
ETFMatchGroup eMatchGroup = ETFMatchGroup_FuzzyParse( args[1] );
|
|
if ( !AssertValidMatchGroup( eMatchGroup ) )
|
|
{
|
|
ConMsg( "Failed to parse a valid match group from input\n" );
|
|
return;
|
|
}
|
|
|
|
if ( GTFPartyClient()->BInQueueForMatchGroup( eMatchGroup ) )
|
|
{
|
|
GTFPartyClient()->CancelMatchQueueRequest( eMatchGroup );
|
|
ConMsg("Requested cancel of queue request for [%s]\n", GetMatchGroupName( eMatchGroup ) );
|
|
}
|
|
else
|
|
{
|
|
ConMsg("Not in queue for [%s], nothing to cancel\n", GetMatchGroupName( eMatchGroup ) );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
//-----------------------------------------------------------------------------
|
|
CON_COMMAND( tf_party_queue_standby_cancel, "Un-queue from standby for your party" )
|
|
{
|
|
if ( args.ArgC() != 1 )
|
|
{
|
|
ConMsg( "Usage: tf_party_queue_standby_cancel - no args\n" );
|
|
return;
|
|
}
|
|
|
|
if (GTFPartyClient()->BInStandbyQueue())
|
|
{
|
|
GTFPartyClient()->CancelStandbyQueueRequest();
|
|
ConMsg("Requested cancel of standby queue request\n");
|
|
}
|
|
else
|
|
{
|
|
ConMsg("Not in standby queue\n");
|
|
}
|
|
}
|
|
#endif // defined( STAGING_ONLY ) || defined( _DEBUG )
|