//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //============================================================================= #include "cbase.h" #include "econ_trading.h" // for messaging with the GC #include "econ_gcmessages.h" #include "econ_item_inventory.h" #include "econ_gcmessages.h" #include "econ_ui.h" // other #include "c_baseplayer.h" #include "c_playerresource.h" // UI #include "confirm_dialog.h" #include "econ_controls.h" #include "vgui/ILocalize.h" #include "econ_notifications.h" #ifdef TF_CLIENT_DLL #include "c_tf_gamestats.h" #endif #include "gc_clientsystem.h" // memdbgon must be the last include file in a .cpp file!!! #include //----------------------------------------------------------------------------- const char *g_FriendRelationship[] = { "none" "blocked", "request_recipient", "friend", "request_initiator", "ignored", "ignored_friend" }; const char *g_RejectedReasons[] = { "accepted", "declined", "vac_banned_initiator", "vac_banned_target", "target_already_trading", "trading_disabled", "user_not_logged_in" }; const char *g_ClosedReasons[] = { "traded", "canceled", "error", "does_not_own_items", "untradable_items", "no_items", "trading_disabled" }; enum { kShowTradeRequestsFrom_FriendsOnly = 1, kShowTradeRequestsFrom_FriendsAndCurrentServer = 2, kShowTradeRequestsFrom_Anyone = 3, kShowTradeRequestsFrom_NoOne = 4, }; ConVar cl_trading_show_requests_from( "cl_trading_show_requests_from", "3", FCVAR_ARCHIVE, "View trade requests from a certain group only." ); static bool sbTestingSelfTrade = false; static int iTradeRequests = 0; static int iTradeAttempts = 0; static int iTradeOffers = 0; static int iGiftsGiven = 0; const uint32 kTradeRequestLifetime = 30.0f; // waiting dialog class CTradingWaitDialog : public CGenericWaitingDialog { public: CTradingWaitDialog( const char *pText = "#TF_Trading_Timeout_Text", const wchar_t *pPlayerName = NULL ) : CGenericWaitingDialog( NULL ) , m_pText( pText ) , m_pKeyValues( NULL ) { if ( pPlayerName != NULL && wcsicmp( pPlayerName, L"" ) != 0 ) { m_pKeyValues = new KeyValues( "CTradingWaitDialog" ); m_pKeyValues->SetWString( "other_player", pPlayerName ); } } virtual ~CTradingWaitDialog() { if ( m_pKeyValues != NULL ) { m_pKeyValues->deleteThis(); } } protected: virtual void OnTimeout() { ShowMessageBox( "#TF_Trading_Timeout_Title", m_pText, m_pKeyValues, "#GameUI_OK" ); } virtual void OnUserClose() { GCSDK::CGCMsg< MsgGCTrading_CancelSession_t > msg( k_EMsgGCTrading_CancelSession ); GCClientSystem()->BSendMessage( msg ); // @note not sure we need to wait for the GC here, but we will just in case... ShowWaitingDialog( new CTradingWaitDialog(), "#TF_Trading_WaitingForCancel", true, false, 20.0f ); } KeyValues *m_pKeyValues; const char* m_pText; }; // used by the waiting dialogs static void TradeCompleteDialogClosed( bool bConfirmed, void *pContext ) { if ( bConfirmed ) { InventoryManager()->ShowItemsPickedUp( true ); } } //----------------------------------------------------------------------------- // jobs class CTFTradeRequestNotification : public CEconNotification { public: CTFTradeRequestNotification( uint64 ulInitiatorSteamID, uint32 unTradeRequestID, const char* pPlayerName ) : CEconNotification() , m_unTradeRequestID( unTradeRequestID ) { SetSteamID( ulInitiatorSteamID ); g_pVGuiLocalize->ConvertANSIToUnicode( pPlayerName, m_wszPlayerName, sizeof(m_wszPlayerName) ); SetLifetime( kTradeRequestLifetime ); SetText( "#TF_Trading_JoinText" ); AddStringToken( "initiator", m_wszPlayerName ); } virtual EType NotificationType() { return eType_AcceptDecline; } /// XXX(JohnS): Dead code? Was always accept/decline AFAICT virtual void Trigger() { CTFGenericConfirmDialog *pDialog = ShowConfirmDialog( "#TF_Trading_JoinTitle", "#TF_Trading_JoinText", "#GameUI_OK", "#TF_Trading_JoinCancel", &ConfirmJoinTradeSession ); pDialog->SetContext( this ); pDialog->AddStringToken( "initiator", m_wszPlayerName ); // so we aren't deleted SetIsInUse( true ); } virtual void Accept() { ConfirmJoinTradeSession( true, this ); } virtual void Decline() { ConfirmJoinTradeSession( false, this ); } static void ConfirmJoinTradeSession( bool bConfirmed, void *pContext ) { CTFTradeRequestNotification *pNotification = (CTFTradeRequestNotification*)pContext; GCSDK::CGCMsg< MsgGCTrading_InitiateTradeResponse_t > msg( k_EMsgGCTrading_InitiateTradeResponse ); msg.Body().m_eResponse = bConfirmed ? k_EGCMsgInitiateTradeResponse_Accepted : k_EGCMsgInitiateTradeResponse_Declined; msg.Body().m_unTradeRequestID = pNotification->m_unTradeRequestID; GCClientSystem()->BSendMessage( msg ); if ( bConfirmed ) { ShowWaitingDialog( new CTradingWaitDialog(), "#TF_Trading_WaitingForServer", true, false, kTradeRequestLifetime ); } // now we can be deleted pNotification->SetIsInUse( false ); pNotification->MarkForDeletion(); } static bool IsTradingNotification( CEconNotification * pNotification ) { return dynamic_cast< CTFTradeRequestNotification * >( pNotification ) != NULL; } public: uint32 m_unTradeRequestID; wchar_t m_wszPlayerName[MAX_PLAYER_NAME_LENGTH]; }; // request from party A (through GC) to start trading class CGCTrading_InitiateTradeRequest : public GCSDK::CGCClientJob { public: CGCTrading_InitiateTradeRequest( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} void SendDeclinedMessage( uint32 unTradeRequestID ) { GCSDK::CGCMsg< MsgGCTrading_InitiateTradeResponse_t > msgResponse( k_EMsgGCTrading_InitiateTradeResponse ); msgResponse.Body().m_eResponse = k_EGCMsgInitiateTradeResponse_Declined; msgResponse.Body().m_unTradeRequestID = unTradeRequestID; GCClientSystem()->BSendMessage( msgResponse ); } virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) { GCSDK::CGCMsg msg( pNetPacket ); CUtlString playerName; msg.BReadStr( &playerName ); if ( sbTestingSelfTrade ) { CloseWaitingDialog(); } else if ( msg.Body().m_ulOtherSteamID == Trading_GetLocalPlayerSteamID().ConvertToUint64() ) { CloseWaitingDialog(); } iTradeRequests++; #ifdef TF_CLIENT_DLL C_CTF_GameStats.Event_Trading( IE_TRADING_REQUEST_RECEIVED, msg.Body().m_ulOtherSteamID, iTradeRequests ); #endif // auto-decline for ignored or blocked peoples if ( steamapicontext == NULL || steamapicontext->SteamFriends() == NULL ) { return true; } CSteamID steamIDOther( msg.Body().m_ulOtherSteamID ); EFriendRelationship eRelationship = steamapicontext->SteamFriends()->GetFriendRelationship( steamIDOther ); switch ( eRelationship ) { case k_EFriendRelationshipBlocked: case k_EFriendRelationshipIgnored: case k_EFriendRelationshipIgnoredFriend: { SendDeclinedMessage( msg.Body().m_unTradeRequestID ); return true; } break; } // switch switch ( cl_trading_show_requests_from.GetInt() ) { case kShowTradeRequestsFrom_FriendsOnly: { if ( eRelationship != k_EFriendRelationshipFriend ) { SendDeclinedMessage( msg.Body().m_unTradeRequestID ); return true; } } break; case kShowTradeRequestsFrom_FriendsAndCurrentServer: { if ( eRelationship == k_EFriendRelationshipFriend ) break; bool bInCurrentGame = false; if ( engine->IsInGame() ) { // otherwise, test if they are in the current game for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) { if ( g_PR && g_PR->IsConnected( iPlayerIndex ) ) { player_info_t pi; if ( !engine->GetPlayerInfo( iPlayerIndex, &pi ) ) continue; if ( !pi.friendsID ) continue; CSteamID steamID( pi.friendsID, 1, GetUniverse(), k_EAccountTypeIndividual ); if ( steamID == steamIDOther ) { bInCurrentGame = true; break; } } } } if ( bInCurrentGame == false ) { SendDeclinedMessage( msg.Body().m_unTradeRequestID ); return true; } } break; case kShowTradeRequestsFrom_Anyone: { // nothing to check } break; case kShowTradeRequestsFrom_NoOne: { SendDeclinedMessage( msg.Body().m_unTradeRequestID ); return true; } break; } NotificationQueue_Add( new CTFTradeRequestNotification( msg.Body().m_ulOtherSteamID, msg.Body().m_unTradeRequestID, playerName.Get() ) ); return true; } protected: }; GC_REG_JOB( GCSDK::CGCClient, CGCTrading_InitiateTradeRequest, "CGCTrading_InitiateTradeRequest", k_EMsgGCTrading_InitiateTradeRequest, GCSDK::k_EServerTypeGCClient ); #ifdef _DEBUG #ifdef CLIENT_DLL CON_COMMAND( cl_trading_test, "Tests the trade ui notification." ) { if ( steamapicontext == NULL || steamapicontext->SteamUser() == NULL ) return; CSteamID steamID = steamapicontext->SteamUser()->GetSteamID(); NotificationQueue_Add( new CTFTradeRequestNotification( steamID.ConvertToUint64(), steamID.ConvertToUint64(), "Biff" ) ); } #endif #endif // _DEBUG /** * Remove notification that matches the trade request */ class CEconNotificationVisitor_RemoveTradeRequest : public CEconNotificationVisitor { public: CEconNotificationVisitor_RemoveTradeRequest( uint32 unTradeRequestID ) : m_unTradeRequestID( unTradeRequestID ) {} virtual void Visit( CEconNotification ¬ification ) { if ( CTFTradeRequestNotification::IsTradingNotification( ¬ification ) ) { CTFTradeRequestNotification &tradeNotification = dynamic_cast< CTFTradeRequestNotification& >( notification ); if ( tradeNotification.m_unTradeRequestID == m_unTradeRequestID ) { tradeNotification.MarkForDeletion(); } } } private: uint32 m_unTradeRequestID; }; // GC notification of trade request status static const char *g_pszTradeResponseDescLocKeys[] = { "#TF_Trading_BusyText", // k_EGCMsgInitiateTradeResponse_Accepted (should never be used!) "#TF_Trading_DeclinedText", // k_EGCMsgInitiateTradeResponse_Declined "#TF_Trading_VACBannedText", // k_EGCMsgInitiateTradeResponse_VAC_Banned_Initiator "#TF_Trading_VACBanned2Text", // k_EGCMsgInitiateTradeResponse_VAC_Banned_Target "#TF_Trading_BusyText", // k_EGCMsgInitiateTradeResponse_Target_Already_Trading "#TF_Trading_DisabledText", // k_EGCMsgInitiateTradeResponse_Disabled "#TF_Trading_NotLoggedIn", // k_EGCMsgInitiateTradeResponse_NotLoggedIn "#TF_Trading_BusyText", // k_EGCMsgInitiateTradeResponse_Cancel (should never be used!) "#TF_Trading_TooSoon", // k_EGCMsgInitiateTradeResponse_TooSoon "#TF_Trading_TooSoonPenalty", // k_EGCMsgInitiateTradeResponse_TooSoonPenalty "#TF_Trading_TradeBannedText", // (was k_EGCMsgInitiateTradeResponse_Free_Account_Initiator_DEPRECATED) "#TF_Trading_TradeBanned2Text", // k_EGCMsgInitiateTradeResponse_Trade_Banned_Target "#TF_Trading_FreeAccountInitiate", // k_EGCMsgInitiateTradeResponse_Free_Account_Initiator "#TF_Trading_SharedAccountInitiate", // k_EGCMsgInitiateTradeResponse_Shared_Account_Initiator "#TF_Trading_Service_Unavailable", // k_EGCMsgInitiateTradeResponse_Service_Unavailable "#TF_Trading_YouBlockedThem", // k_EGCMsgInitiateTradeResponse_Target_Blocked "#TF_Trading_NeedVerifiedEmail", // k_EGCMsgInitiateTradeResponse_NeedVerifiedEmail "#TF_Trading_NeedSteamGuard", // k_EGCMsgInitiateTradeResponse_NeedSteamGuard "#TF_Trading_SteamGuardDuration", // k_EGCMsgInitiateTradeResponse_SteamGuardDuration "#TF_Trading_TheyCannotTrade", // k_EGCMsgInitiateTradeResponse_TheyCannotTrade "#TF_Trading_PasswordChanged", // k_EGCMsgInitiateTradeResponse_Recent_Password_Reset = 20, "#TF_Trading_NewDevice", // k_EGCMsgInitiateTradeResponse_Using_New_Device = 21, "#TF_Trading_InvalidCookie" // k_EGCMsgInitiateTradeResponse_Sent_Invalid_Cookie = 22, }; class CGCTrading_InitiateTradeResponse : public GCSDK::CGCClientJob { public: CGCTrading_InitiateTradeResponse( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) { // If this assertion fails it probably means you added a new value to EGCMsgInitiateTradeResponse // but didn't add a string to the array that tracks the user-facing response strings. Assert( ARRAYSIZE( g_pszTradeResponseDescLocKeys ) == k_EGCMsgInitiateTradeResponse_Count ); CloseWaitingDialog(); GCSDK::CGCMsg msg( pNetPacket ); const uint32 eResponse = msg.Body().m_eResponse; switch ( eResponse ) { case k_EGCMsgInitiateTradeResponse_Accepted: #ifdef TF_CLIENT_DLL C_CTF_GameStats.Event_Trading( IE_TRADING_REQUEST_ACCEPTED, iTradeRequests, g_RejectedReasons[msg.Body().m_eResponse] ); #endif ShowWaitingDialog( new CTradingWaitDialog(), "#TF_Trading_WaitingForStart", true, false, 30.0f ); return true; // ! case k_EGCMsgInitiateTradeResponse_Cancel: { CEconNotificationVisitor_RemoveTradeRequest visitor( msg.Body().m_unTradeRequestID ); NotificationQueue_Visit( visitor ); break; } case k_EGCMsgInitiateTradeResponse_Declined: case k_EGCMsgInitiateTradeResponse_VAC_Banned_Initiator: case k_EGCMsgInitiateTradeResponse_VAC_Banned_Target: case k_EGCMsgInitiateTradeResponse_Target_Already_Trading: case k_EGCMsgInitiateTradeResponse_Disabled: case k_EGCMsgInitiateTradeResponse_NotLoggedIn: case k_EGCMsgInitiateTradeResponse_TooSoon: case k_EGCMsgInitiateTradeResponse_TooSoonPenalty: case k_EGCMsgInitiateTradeResponse_Trade_Banned_Initiator: case k_EGCMsgInitiateTradeResponse_Trade_Banned_Target: case k_EGCMsgInitiateTradeResponse_Shared_Account_Initiator: case k_EGCMsgInitiateTradeResponse_Service_Unavailable: case k_EGCMsgInitiateTradeResponse_Target_Blocked: case k_EGCMsgInitiateTradeResponse_NeedVerifiedEmail: case k_EGCMsgInitiateTradeResponse_NeedSteamGuard: case k_EGCMsgInitiateTradeResponse_TheyCannotTrade: case k_EGCMsgInitiateTradeResponse_Recent_Password_Reset: case k_EGCMsgInitiateTradeResponse_Using_New_Device: case k_EGCMsgInitiateTradeResponse_Sent_Invalid_Cookie: ShowMessageBox( "#TF_Trading_StatusTitle", g_pszTradeResponseDescLocKeys[eResponse], "#GameUI_OK" ); break; case k_EGCMsgInitiateTradeResponse_SteamGuardDuration: { KeyValuesAD kvTokens( "CTradingWaitDialog" ); kvTokens->SetWString( "days", L"15" ); // Ideally this would come from the GC, which would get the value from Steam ShowMessageBox( "#TF_Trading_StatusTitle", g_pszTradeResponseDescLocKeys[eResponse], kvTokens, "#GameUI_OK" ); break; } default: #ifdef TF_CLIENT_DLL C_CTF_GameStats.Event_Trading( IE_TRADING_REQUEST_REJECTED, iTradeRequests, "unknown" ); #endif ShowMessageBox( "#TF_Trading_StatusTitle", "#TF_Trading_BusyText", "#GameUI_OK" ); return true; } // switch #ifdef TF_CLIENT_DLL C_CTF_GameStats.Event_Trading( IE_TRADING_REQUEST_REJECTED, iTradeRequests, g_RejectedReasons[msg.Body().m_eResponse] ); #endif return true; } }; GC_REG_JOB( GCSDK::CGCClient, CGCTrading_InitiateTradeResponse, "CGCTrading_InitiateTradeResponse", k_EMsgGCTrading_InitiateTradeResponse, GCSDK::k_EServerTypeGCClient ); // start trading session class CGCTrading_StartSession : public GCSDK::CGCClientJob { public: CGCTrading_StartSession( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) { GCSDK::CGCMsg msg( pNetPacket ); steamapicontext->SteamFriends()->ActivateGameOverlayToUser( "jointrade", msg.Body().m_ulSteamIDPartyB ); CloseWaitingDialog(); // remove all trading notifications NotificationQueue_Remove( &CTFTradeRequestNotification::IsTradingNotification ); return true; } }; GC_REG_JOB( GCSDK::CGCClient, CGCTrading_StartSession, "CGCTrading_StartSession", k_EMsgGCTrading_StartSession, GCSDK::k_EServerTypeGCClient ); //----------------------------------------------------------------------------- // External interface CSteamID Trading_GetLocalPlayerSteamID() { if ( steamapicontext && steamapicontext->SteamUser() ) { return steamapicontext->SteamUser()->GetSteamID(); } return CSteamID(); } void Trading_RequestTrade( int iPlayerIdx ) { CSteamID steamID; C_BasePlayer *pPlayer = ToBasePlayer( UTIL_PlayerByIndex( iPlayerIdx ) ); if ( pPlayer && pPlayer->GetSteamID( &steamID ) ) { Trading_RequestTrade( steamID ); } } void Trading_RequestTrade( const CSteamID &steamID ) { sbTestingSelfTrade = false; GCSDK::CGCMsg< MsgGCTrading_InitiateTradeRequest_t > msg( k_EMsgGCTrading_InitiateTradeRequest ); msg.Body().m_ulOtherSteamID = steamID.ConvertToUint64(); bool bSent = GCClientSystem()->BSendMessage( msg ); if ( bSent ) { iTradeRequests++; #ifdef TF_CLIENT_DLL C_CTF_GameStats.Event_Trading( IE_TRADING_REQUEST_SENT, msg.Body().m_ulOtherSteamID, iTradeRequests ); #endif const char* pPlayerName = InventoryManager()->PersonaName_Get( steamID.GetAccountID() ); if ( pPlayerName != NULL && FStrEq( pPlayerName, "" ) == false ) { wchar_t wszPlayerName[MAX_PLAYER_NAME_LENGTH]; g_pVGuiLocalize->ConvertANSIToUnicode( pPlayerName, wszPlayerName, sizeof(wszPlayerName) ); CTradingWaitDialog *pDialog = new CTradingWaitDialog( "#TF_Trading_TimeoutPartyB_Named", wszPlayerName ); ShowWaitingDialog( pDialog, "#TF_Trading_WaitingForPartyB", true, true, 30.0f ); wchar_t wszConstructedString[1024]; g_pVGuiLocalize->ConstructString_safe( wszConstructedString, g_pVGuiLocalize->Find( "#TF_Trading_WaitingForPartyB_Named" ), 1, wszPlayerName ); pDialog->SetDialogVariable( "updatetext", wszConstructedString ); } else { CTradingWaitDialog *pDialog = new CTradingWaitDialog( "#TF_Trading_TimeoutPartyB" ); ShowWaitingDialog( pDialog, "#TF_Trading_WaitingForPartyB", true, true, 30.0f ); } } } const char* UniverseToCommunityURL( EUniverse universe ) { switch( universe ) { default: // return public if we don't have a better guess. case k_EUniversePublic: return "https://steamcommunity.com"; case k_EUniverseBeta: return "https://beta.steamcommunity.com"; case k_EUniverseDev: return "https://localhost/community"; } // Should never get here. return UniverseToCommunityURL( k_EUniversePublic ); } const char* GetCommunityURL() { if ( GetUniverse() == k_EUniverseInvalid ) { Assert( !"calling GetCommunityURL when not connected. This is allowed, but will return public universe." ); return UniverseToCommunityURL( k_EUniversePublic ); } return UniverseToCommunityURL( GetUniverse() ); } void Trading_SendGift( const CSteamID& steamID, const CEconItemView& giftItem ) { if ( !steamapicontext || !steamapicontext->SteamFriends() ) { // TODO: Error dialog. return; } #ifdef TF_CLIENT_DLL C_CTF_GameStats.Event_Trading( IE_TRADING_ITEM_GIFTED, steamID.ConvertToUint64(), iGiftsGiven ); #endif // Build up the steam URL and send it over. // Should look like this: https://steamcommunity.com/trade/1/sendgift/?appid=&contextid=&assetid=&steamid_target= steamapicontext->SteamFriends()->ActivateGameOverlayToWebPage( CFmtStrMax( "%s/trade/1/sendgift/?appid=%d&contextid=%d&assetid=%llu&steamid_target=%llu", GetCommunityURL(), engine->GetAppID(), 2, // k_EEconContextBackpack giftItem.GetItemID(), steamID.ConvertToUint64() ) ); } CON_COMMAND( cl_trade, "Trade with a person by player name" ) { if ( args.ArgC() < 2 ) return; if ( GetUniverse() == k_EUniverseInvalid ) return; int iLocalPlayerIndex = GetLocalPlayerIndex(); for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) { if( ( iPlayerIndex != iLocalPlayerIndex ) && ( g_PR->IsConnected( iPlayerIndex ) ) ) { player_info_t pi; if ( !engine->GetPlayerInfo( iPlayerIndex, &pi ) ) continue; if ( !pi.friendsID ) continue; if ( FStrEq( pi.name, args[1] ) == false ) continue; CSteamID steamID( pi.friendsID, 1, GetUniverse(), k_EAccountTypeIndividual ); Trading_RequestTrade( steamID ); return; } } } CON_COMMAND( cl_trade_steamid, "Trade with a person by steam id" ) { if ( args.ArgC() < 2 ) return; if ( GetUniverse() == k_EUniverseInvalid ) return; const char *pInput = args[1]; if ( pInput[0] >= '0' && pInput[0] <= '9' ) { CSteamID steamID; steamID.SetFromString( pInput, GetUniverse() ); if ( steamID.IsValid() ) Trading_RequestTrade( steamID ); } } #ifdef _DEBUG CON_COMMAND( cl_trading_test_self_trade, "Test self-trading" ) { Trading_RequestTrade( Trading_GetLocalPlayerSteamID() ); sbTestingSelfTrade = true; } #endif