diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/client/tf/tf_consumables.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/client/tf/tf_consumables.cpp')
| -rw-r--r-- | game/client/tf/tf_consumables.cpp | 2233 |
1 files changed, 2233 insertions, 0 deletions
diff --git a/game/client/tf/tf_consumables.cpp b/game/client/tf/tf_consumables.cpp new file mode 100644 index 0000000..3508b8b --- /dev/null +++ b/game/client/tf/tf_consumables.cpp @@ -0,0 +1,2233 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//============================================================================= +#include "cbase.h" + +// for messaging with the GC +#include "econ_gcmessages.h" +#include "econ_item_inventory.h" +#include "tf_gcmessages.h" +#include "tf_duel_summary.h" +#include "gc_clientsystem.h" + +// other +#include "c_playerresource.h" +#include "c_tf_player.h" +#include "tf_item_wearable.h" +#include "econ_notifications.h" +#include "tf_hud_chat.h" +#include "c_tf_gamestats.h" +#include "tf_gamerules.h" +#include "tf_item_tools.h" +#include "c_tf_freeaccount.h" +#include "tf_item_powerup_bottle.h" +#include "tf_weapon_grapplinghook.h" + +// for UI +#include "clientmode_tf.h" +#include "confirm_dialog.h" +#include "select_player_dialog.h" +#include "econ_notifications.h" +#include "vgui/ISurface.h" +#include "vgui/character_info_panel.h" +#include "tf_hud_mainmenuoverride.h" +#include "econ_ui.h" +#include "backpack_panel.h" +#include "store/v1/tf_store_page.h" +#include "econ_item_description.h" +#include "weapon_selection.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include <tier0/memdbgon.h> + +//----------------------------------------------------------------------------- +// Wrapped Gift Declarations + +void UseGift( CEconItemView* pItem, CSteamID targetID ); +extern void PerformToolAction_UnwrapGift( vgui::Panel* pParent, CEconItemView *pGiftItem ); +extern void ShowWaitingDialog( CGenericWaitingDialog *pWaitingDialog, const char* pUpdateText, bool bAnimate, bool bShowCancel, float flMaxDuration ); +class CDeliverGiftSelectDialog; +CDeliverGiftSelectDialog *OpenDeliverGiftDialog( vgui::Panel *pParent, CEconItemView *pItem ); +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Duel Declarations +bool DuelMiniGame_IsDuelingLocalPlayer( C_TFPlayer *pPlayer ); +bool DuelMiniGame_IsDueling(); +//----------------------------------------------------------------------------- + +class CWaitForPackageDialog : public CGenericWaitingDialog +{ +public: + CWaitForPackageDialog( vgui::Panel *pParent ) : CGenericWaitingDialog( pParent ) + { + } + +protected: + virtual void OnTimeout() + { + // Play an exciting sound! + vgui::surface()->PlaySound( "misc/achievement_earned.wav" ); + + // Show them their loot! + InventoryManager()->ShowItemsPickedUp( true ); + } +}; + +bool IgnoreRequestFromUser( const CSteamID &steamID ) +{ + // ignore blocked players + if ( steamapicontext && steamapicontext->SteamFriends() ) + { + EFriendRelationship eRelationship = steamapicontext->SteamFriends()->GetFriendRelationship( steamID ); + switch ( eRelationship ) + { + case k_EFriendRelationshipBlocked: + { + return true; + } + } + } + return false; +} + +static void ShowSelectDuelTargetDialog( uint64 iItemID ); + +enum EServerPlayersGCSend +{ + kServerPlayers_DontSend, + kServerPlayers_Send, +}; + +struct CUseItemConfirmContext +{ +public: + CUseItemConfirmContext( CEconItemView *pEconItemView, EServerPlayersGCSend eSendServerPlayers, const char* pszConfirmUseSound = NULL ) + : m_pEconItemView( pEconItemView ) + , m_bSendServerPlayers( eSendServerPlayers == kServerPlayers_Send ) + , m_pszConfirmUseSound( pszConfirmUseSound ) + { + Assert( eSendServerPlayers == kServerPlayers_DontSend || eSendServerPlayers == kServerPlayers_Send ); + } + + void OnConfirmUse() + { + if ( m_pszConfirmUseSound && *m_pszConfirmUseSound ) + { + vgui::surface()->PlaySound( m_pszConfirmUseSound ); + } + } + + const char* m_pszConfirmUseSound; + CEconItemView *m_pEconItemView; + bool m_bSendServerPlayers; +}; + +static void UseItemConfirm( bool bConfirmed, void *pContext ) +{ + static CSchemaAttributeDefHandle pAttrDef_UnlimitedUse( "unlimited quantity" ); + + CUseItemConfirmContext *pConfirmContext = (CUseItemConfirmContext *)pContext; + CEconItemView *pEconItemView = pConfirmContext->m_pEconItemView; + + if ( bConfirmed ) + { + pConfirmContext->OnConfirmUse(); + + if ( pEconItemView && !pEconItemView->FindAttribute( pAttrDef_UnlimitedUse ) ) + { + GCSDK::CProtoBufMsg<CMsgUseItem> msg( k_EMsgGCUseItemRequest ); + msg.Body().set_item_id( pConfirmContext->m_pEconItemView->GetItemID() ); + + if ( pConfirmContext->m_bSendServerPlayers ) + { + for ( int i = 1; i <= gpGlobals->maxClients; i++ ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); + if ( pPlayer == NULL ) + continue; + + CSteamID steamIDPlayer; + if ( !pPlayer->GetSteamID( &steamIDPlayer ) ) + continue; + + msg.Body().add_gift__potential_targets( steamIDPlayer.GetAccountID() ); + } + } + + GCClientSystem()->BSendMessage( msg ); + } + + EconUI()->Gamestats_ItemTransaction( IE_ITEM_USED_CONSUMABLE, pEconItemView, NULL ); + + // CBaseHudChat *pHUDChat = (CBaseHudChat *)GET_HUDELEMENT( CHudChat ); + // if ( pHUDChat ) + // { + // char szAnsi[1024]; + // Q_snprintf( szAnsi, 1024, "Using item: %ull", pItem->GetItemID() ); + // pHUDChat->Printf( CHAT_FILTER_NONE, "%s", szAnsi ); + // } + } + delete pConfirmContext; +} + +static void OpenPass( bool bConfirmed, void *pContext ) +{ + if ( bConfirmed ) + { + vgui::surface()->PlaySound( "ui/item_gift_wrap_unwrap.wav" ); + ShowWaitingDialog( new CWaitForPackageDialog( NULL ), "#ToolRedeemingPass", true, false, 5.0f ); + } + UseItemConfirm( bConfirmed, pContext ); +} + +static void PrintTextToChat( const char *pText, KeyValues *pKeyValues ) +{ + GetClientModeTFNormal()->PrintTextToChat( pText, pKeyValues ); +} + +void GetPlayerNameBySteamID( const CSteamID &steamID, OUT_Z_CAP(maxLenInChars) char *pDestBuffer, int maxLenInChars ) +{ + // always attempt to precache this user's name -- we may need it later after they leave the server, for instance, + // even if that's where they are now + InventoryManager()->PersonaName_Precache( steamID.GetAccountID() ); + + // first, look through players on our connected gameserver if available -- we already have this information and + // if there's a disagreement between Steam and the gameserver the gameserver view is what the player is probably + // expecting anyway + if ( engine->IsInGame() ) + { + for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) + { + if ( g_PR->IsConnected( iPlayerIndex ) ) + { + player_info_t pi; + if ( !engine->GetPlayerInfo( iPlayerIndex, &pi ) ) + continue; + if ( !pi.friendsID ) + continue; + + CSteamID steamIDTemp( pi.friendsID, 1, GetUniverse(), k_EAccountTypeIndividual ); + if ( steamIDTemp == steamID ) + { + V_strncpy( pDestBuffer, pi.name, maxLenInChars ); + return; + } + } + } + } + + // try the persona name cache + // this goes to steam if necessary + const char *pszName = InventoryManager()->PersonaName_Get( steamID.GetAccountID() ); + if ( pszName != NULL ) + { + V_strncpy( pDestBuffer, pszName, maxLenInChars ); + return; + } + + // otherwise, return what we would normally return + V_strncpy( pDestBuffer, steamapicontext->SteamFriends()->GetFriendPersonaName( steamID ), maxLenInChars ); +} + +static bool IsGCUseableItem( const GameItemDefinition_t *pItemDef ) +{ + Assert( pItemDef ); + + return (pItemDef->GetCapabilities() & ITEM_CAP_USABLE_GC) != 0; +} + +//----------------------------------------------------------------------------- +// Purpose: Implementation of the local response for someone who used a dueling minigame +//----------------------------------------------------------------------------- +void CEconTool_DuelingMinigame::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + Assert( pItem ); + + C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); + Assert( pLocalPlayer ); + + if ( DuelMiniGame_IsDueling() ) + { + // can't duel if already dueling + ShowMessageBox( "#TF_Duel_Title", "#TF_Duel_InADuel_Initiator", "#GameUI_OK" ); + return; + } + if ( pLocalPlayer->GetTeamNumber() != TF_TEAM_RED && pLocalPlayer->GetTeamNumber() != TF_TEAM_BLUE ) + { + // can't duel from spectator mode, etc. + ShowMessageBox( "#TF_UseFail_NotOnTeam_Title", "#TF_UseFail_NotOnTeam", "#GameUI_OK" ); + return; + } + if ( TFGameRules() && !TFGameRules()->CanInitiateDuels() ) + { + ShowMessageBox( "#TF_Duel_Title", "#TF_Duel_CannotUse", "#GameUI_OK" ); + return; + } + + ShowSelectDuelTargetDialog( pItem->GetItemID() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Implementation of the local response for someone who used a noisemaker +//----------------------------------------------------------------------------- +void CEconTool_Noisemaker::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + Assert( pItem ); + + C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); + Assert( pLocalPlayer ); + + if ( gpGlobals->curtime < pLocalPlayer->m_Shared.GetNextNoiseMakerTime() ) + return; + + if ( !pLocalPlayer->IsAlive() ) + return; + + if ( pLocalPlayer->GetTeamNumber() < FIRST_GAME_TEAM ) + return; + + // This may not be ideal. We're going to have the game server do the noise effect, + // without checking the GC to see whether we have charges available. Querying the + // GC would cause a significant delay before the item was used, so we simulate. + + // Tell the game server to play the sound. + KeyValues *kv = new KeyValues( "use_action_slot_item_server" ); + engine->ServerCmdKeyValues( kv ); + + // Tell the GC to consume a charge. + CUseItemConfirmContext *context = new CUseItemConfirmContext( pItem, kServerPlayers_DontSend ); + UseItemConfirm( true, context ); + + // Notify the player that they used their last charge. + if ( pItem->GetItemQuantity() <= 1 ) + { + CEconNotification *pNotification = new CEconNotification(); + pNotification->SetText( "#TF_NoiseMaker_Exhausted" ); + pNotification->SetLifetime( 7.0f ); + NotificationQueue_Add( pNotification ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Implementation of the local response for someone who used a gift-wrapped item +//----------------------------------------------------------------------------- +void UseUntargetedGiftConfirm( bool bConfirmed, void *pContext ) +{ + if ( bConfirmed ) + { + UseGift( static_cast<CEconItemView *>( pContext ), k_steamIDNil ); + } +} + +void CEconTool_WrappedGift::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + Assert( pItem ); + + if ( BIsDirectGift() ) + { + OpenDeliverGiftDialog( pParent, pItem ); + } + else + { + // ...otherwise, we should try and open the gift! + PerformToolAction_UnwrapGift( pParent, pItem ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEconTool_WeddingRing::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + Assert( pItem ); + + // Don't do anything if we haven't been gifted already -- we don't expect to + // ever get in here, really. + static CSchemaAttributeDefHandle pAttrDef_GifterAccountID( "gifter account id" ); + + uint32 unAccountID; + if ( !pItem->FindAttribute( pAttrDef_GifterAccountID, &unAccountID ) ) + return; + + // We have been gifted, so pop up a dialog box to allow the user to accept/reject + // the proposal. + CTFGenericConfirmDialog *pDialog = ShowConfirmDialog( "#TF_UseWeddingRing_Title", "#TF_UseWeddingRing_Text", + "#TF_WeddingRing_AcceptProposal", "#TF_WeddingRing_RejectProposal", + &UseItemConfirm ); + + pDialog->AddStringToken( "item_name", pItem->GetItemName() ); + + CUtlConstWideString sProposerPersonaName; + GLocalizationProvider()->ConvertUTF8ToLocchar( TFInventoryManager()->PersonaName_Get( unAccountID ), &sProposerPersonaName ); + pDialog->AddStringToken( "proposer_name", sProposerPersonaName.Get() ); + + pDialog->SetContext( new CUseItemConfirmContext( pItem, kServerPlayers_DontSend ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: Implementation of the local response for someone who used a backpack expander +//----------------------------------------------------------------------------- +void CEconTool_BackpackExpander::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + Assert( pItem ); + + // first validate that they aren't already at max inventory size and can use the item + uint32 unExtraSlots =GetBackpackSlots(); + if ( unExtraSlots == 0 ) + { + return; + } + uint32 unNewNumSlots = TFInventoryManager()->GetLocalTFInventory()->GetMaxItemCount() + unExtraSlots; + if ( unNewNumSlots > MAX_NUM_BACKPACK_SLOTS ) + { + ShowMessageBox( "#TF_UseBackpackExpanderFail_Title", "#TF_UseBackpackExpanderFail_Text", "#GameUI_OK" ); + return; + } + // Free Trials can use expanders but max out at a smaller value since premium gains a bunch of free slots + if ( IsFreeTrialAccount() && unNewNumSlots > MAX_NUM_BACKPACK_SLOTS - (DEFAULT_NUM_BACKPACK_SLOTS - DEFAULT_NUM_BACKPACK_SLOTS_FREE_TRIAL_ACCOUNT) ) + { + ShowMessageBox( "#TF_UseBackpackExpanderFail_Title", "#TF_UseBackpackExpanderFail_Text", "#GameUI_OK" ); + return; + } + + CTFGenericConfirmDialog *pDialog = ShowConfirmDialog( "#TF_UseBackpackExpander_Title", "#TF_UseBackpackExpander_Text", + "#GameUI_OK", "#Cancel", + &UseItemConfirm ); + pDialog->AddStringToken( "item_name", pItem->GetItemName() ); + wchar_t wszUsesLeft[32]; + _snwprintf( wszUsesLeft, ARRAYSIZE(wszUsesLeft), L"%d", pItem->GetItemQuantity() ); + pDialog->AddStringToken( "uses_left", wszUsesLeft ); + wchar_t wszNewSize[32]; + _snwprintf( wszNewSize, ARRAYSIZE(wszNewSize), L"%d", unNewNumSlots ); + pDialog->AddStringToken( "new_size", wszNewSize ); + pDialog->SetContext( new CUseItemConfirmContext( pItem, kServerPlayers_DontSend ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: Implementation of the local response for someone who used an account upgrade +//----------------------------------------------------------------------------- +void CEconTool_AccountUpgradeToPremium::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + Assert( pItem ); + + // if the account is already premium, abort here + if ( !IsFreeTrialAccount() ) + { + ShowMessageBox( "#TF_UseAccountUpgradeToPremiumFail_Title", "#TF_UseAccountUpgradeToPremiumFail_Text", "#GameUI_OK" ); + return; + } + + // show a confirmation dialog to make sure they want to consume the charge + CTFGenericConfirmDialog *pDialog = ShowConfirmDialog( "#TF_UseAccountUpgradeToPremium_Title", "#TF_UseAccountUpgradeToPremium_Text", + "#GameUI_OK", "#Cancel", + &UseItemConfirm ); + pDialog->AddStringToken( "item_name", pItem->GetItemName() ); + wchar_t wszUsesLeft[32]; + _snwprintf( wszUsesLeft, ARRAYSIZE(wszUsesLeft), L"%d", pItem->GetItemQuantity() ); + pDialog->AddStringToken( "uses_left", wszUsesLeft ); + pDialog->SetContext( new CUseItemConfirmContext( pItem, kServerPlayers_DontSend ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: Implementation of the local response for someone who used a claim code item +//----------------------------------------------------------------------------- +void CEconTool_ClaimCode::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + Assert( pItem ); + + CTFGenericConfirmDialog *pDialog = ShowConfirmDialog( "#TF_UseClaimCode_Title", "#TF_UseClaimCode_Text", + "#GameUI_OK", "#Cancel", + &UseItemConfirm ); + pDialog->AddStringToken( "item_name", pItem->GetItemName() ); + + const char *pszClaimValue = GetClaimType(); + if ( pszClaimValue ) + { + wchar_t wszClaimType[128]; + KeyValuesAD pkvDummy( "dummy" ); + g_pVGuiLocalize->ConstructString_safe( wszClaimType, pszClaimValue, pkvDummy ); + pDialog->AddStringToken( "claim_type", wszClaimType ); + } + pDialog->SetContext( new CUseItemConfirmContext( pItem, kServerPlayers_DontSend ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: Implementation of the local response for someone who used an item that doesn't have +// special-case handling (ie., paint); called into from other code +//----------------------------------------------------------------------------- +static bool s_bConsumableToolOpeningGift = false; +static void ClientConsumableTool_Generic( CEconItemView *pItem, vgui::Panel *pParent ) +{ + Assert( pItem ); + Assert( pItem->GetItemDefinition() ); + Assert( pItem->GetItemDefinition()->GetEconTool() ); + + CTFGenericConfirmDialog *pDialog = ShowConfirmDialog( "#TF_UseItem_Title", "#TF_UseItem_Text", + "#GameUI_OK", "#Cancel", + &UseItemConfirm ); + pDialog->AddStringToken( "item_name", pItem->GetItemName() ); + wchar_t wszUsesLeft[32]; + _snwprintf( wszUsesLeft, ARRAYSIZE(wszUsesLeft), L"%d", pItem->GetItemQuantity() ); + pDialog->AddStringToken( "uses_left", wszUsesLeft ); + pDialog->SetContext( + new CUseItemConfirmContext( pItem, + pItem->GetItemDefinition()->GetTypedEconTool<CEconTool_Gift>() + ? kServerPlayers_Send + : kServerPlayers_DontSend ) ); + + // Minor Hack to get sound to play differently. Add a look up table + s_bConsumableToolOpeningGift = false; + const CEconTool_Gift *pGiftTool = pItem->GetItemDefinition()->GetTypedEconTool<CEconTool_Gift>(); + if ( pGiftTool && pGiftTool->GetTargetRule() == kGiftTargetRule_OnlySelf) + { + s_bConsumableToolOpeningGift = true; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Generic Response +//----------------------------------------------------------------------------- +void CEconTool_Xifier::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + ClientConsumableTool_Generic( pItem, pParent ); +} + +//----------------------------------------------------------------------------- +void CEconTool_ItemEaterRecharger::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + // Tell the GC to consume a charge. + CUseItemConfirmContext *context = new CUseItemConfirmContext( pItem, kServerPlayers_DontSend ); + UseItemConfirm( true, context ); +} + +//----------------------------------------------------------------------------- +// Purpose: Implementation of the local response for someone who used (tried to redeem) a collection +//----------------------------------------------------------------------------- +void CEconTool_PaintCan::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + ClientConsumableTool_Generic( pItem, pParent ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEconTool_Gift::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + ClientConsumableTool_Generic( pItem, pParent ); +} + +//----------------------------------------------------------------------------- +// Purpose: Implementation of the local response for someone who used (tried to redeem) a collection +//----------------------------------------------------------------------------- +void CEconTool_Default::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + ClientConsumableTool_Generic( pItem, pParent ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEconTool_TFEventEnableHalloween::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + Assert( pItem ); + + // Tell the GC we want to use this item. + GCSDK::CProtoBufMsg<CMsgGC_Client_UseServerModificationItem> msg( k_EMsgGC_Client_UseServerModificationItem ); + msg.Body().set_item_id( pItem->GetItemID() ); + + GCClientSystem()->BSendMessage( msg ); +} + +//----------------------------------------------------------------------------- +void CEconTool_DuckToken::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + ClientConsumableTool_Generic( pItem, pParent ); +} +//----------------------------------------------------------------------------- +void CEconTool_GrantOperationPass::OnClientUseConsumable( CEconItemView *pItem, vgui::Panel *pParent ) const +{ + Assert( pItem ); + const CEconTool_GrantOperationPass *pEconToolOperationPass = pItem->GetItemDefinition()->GetTypedEconTool<CEconTool_GrantOperationPass>(); + if ( !pEconToolOperationPass ) + { + ShowMessageBox( "#TF_UseOperationPassFail_Title", "#TF_UseOperationPassFail_Text", "#GameUI_OK" ); + return; + } + + // Check that the player doesn't already have an active pass + const char *szPassName = pEconToolOperationPass->m_pOperationPassName; + CEconItemDefinition *pActivePassItemDef = GetItemSchema()->GetItemDefinitionByName( szPassName ); + CPlayerInventory *pLocalInv = TFInventoryManager()->GetLocalInventory(); + if ( !pLocalInv || !pActivePassItemDef ) + { + ShowMessageBox( "#TF_UseOperationPassFail_Title", "#TF_UseOperationPassFail_Text", "#GameUI_OK" ); + return; + } + + for ( int i = 0; i < pLocalInv->GetItemCount(); ++i ) + { + CEconItemView *pItemLocal = pLocalInv->GetItem( i ); + Assert( pItemLocal ); + if ( pItemLocal->GetItemDefinition() == pActivePassItemDef ) + { + ShowMessageBox( "#TF_UseOperationPassAlreadyActive_Title", "#TF_UseOperationPassAlreadyActive_Text", "#GameUI_OK" ); + return; + } + } + + vgui::surface()->PlaySound( "ui/quest_operation_pass_buy.wav" ); + + const char *pszTitle = "#TF_UseOperationPass_Title"; + const char *pszBody = "#TF_UseOperationPass_Text"; + + static CSchemaItemDefHandle pItemDef_InvasionPass( "Unused Invasion Pass" ); + if ( pItem->GetItemDefinition() == pItemDef_InvasionPass ) + { + pszBody = "#TF_UseInvasionPass_Text"; + } + + // show a confirmation dialog to make sure they want to consume the charge + CTFGenericConfirmDialog *pDialog = ShowConfirmDialog( pszTitle, pszBody, + "#GameUI_OK", "#Cancel", + &OpenPass ); + pDialog->AddStringToken( "item_name", pItem->GetItemName() ); + wchar_t wszUsesLeft[32]; + _snwprintf( wszUsesLeft, ARRAYSIZE( wszUsesLeft ), L"%d", pItem->GetItemQuantity() ); + pDialog->AddStringToken( "uses_left", wszUsesLeft ); + pDialog->SetContext( new CUseItemConfirmContext( pItem, kServerPlayers_DontSend, "ui/quest_operation_pass_use.wav" ) ); +} +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CGCEventEnableResponse : public GCSDK::CGCClientJob +{ +public: + CGCEventEnableResponse( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} + virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) + { + GCSDK::CProtoBufMsg<CMsgGC_Client_UseServerModificationItem_Response> msg( pNetPacket ); + + switch ( msg.Body().response_code() ) + { + case CMsgGC_Client_UseServerModificationItem_Response::kServerModificationItemResponse_AlreadyInUse: + ShowMessageBox( "#TF_ServerEnchantmentType", "#TF_Eternaween__AlreadyInUse", (KeyValues *)NULL ); + break; + + case CMsgGC_Client_UseServerModificationItem_Response::kServerModificationItemResponse_NotOnAuthenticatedServer: + ShowMessageBox( "#TF_ServerEnchantmentType", "#TF_Eternaween__AuthenticatedServerRequired", (KeyValues *)NULL ); + break; + + case CMsgGC_Client_UseServerModificationItem_Response::kServerModificationItemResponse_ServerReject: + ShowMessageBox( "#TF_ServerEnchantmentType", "#TF_Eternaween__ServerReject", (KeyValues *)NULL ); + break; + + case CMsgGC_Client_UseServerModificationItem_Response::kServerModificationItemResponse_InternalError: + ShowMessageBox( "#TF_ServerEnchantmentType", "#TF_Eternaween__InternalError", (KeyValues *)NULL ); + break; + + case CMsgGC_Client_UseServerModificationItem_Response::kServerModificationItemResponse_EventAlreadyActive: + ShowMessageBox( "#TF_ServerEnchantmentType", "#TF_Eternaween__EventAlreadyActive", (KeyValues *)NULL ); + break; + } + + return true; + } +}; +GC_REG_JOB( GCSDK::CGCClient, CGCEventEnableResponse, "CGCEventEnableResponse", k_EMsgGC_Client_UseServerModificationItem_Response, GCSDK::k_EServerTypeGCClient ); + +// invoked when the local player attempts to consume the given item +void UseConsumableItem( CEconItemView *pItem, vgui::Panel *pParent ) +{ + Assert( pItem ); + + C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); + + const GameItemDefinition_t *pItemDef = pItem->GetStaticData(); + Assert( pItemDef ); + + bool bUsableOutOfGame = (pItemDef->GetCapabilities() & ITEM_CAP_USABLE_OUT_OF_GAME) != 0; + + // if we aren't useable outside of the game then make sure that we're in a game and that + // we have a local player we can use + if ( !bUsableOutOfGame ) + { + if ( !engine->IsInGame() ) + { + ShowMessageBox( "#TF_UseFail_NotInGame_Title", "#TF_UseFail_NotInGame", "#GameUI_OK" ); + return; + } + + if ( pLocalPlayer == NULL ) + return; + } + + // make sure this item meets our baseline useable criteria + if ( pItem->GetItemQuantity() <= 0 ) + return; + + if ( !IsGCUseableItem( pItemDef ) ) + return; + + const IEconTool *pEconTool = pItemDef->GetEconTool(); + if ( !pEconTool ) + return; + + // do whatever client work needs to be done, send a request to the GC to use the item, etc. + pEconTool->OnClientUseConsumable( pItem, pParent ); +} + +// Called from the trade dialog when the player selects a target user ID. +void UseGift( CEconItemView* pItem, CSteamID targetID ) +{ + // Validate pItem... + if ( !pItem ) + return; + + const GameItemDefinition_t *pItemDef = pItem->GetItemDefinition(); + if ( !pItemDef ) + return; + + if ( !IsGCUseableItem( pItemDef ) ) + return; + + if ( !pItemDef->GetTypedEconTool<CEconTool_WrappedGift>() ) + return; + + GCSDK::CGCMsg< MsgGCDeliverGift_t > msg( k_EMsgGCDeliverGift ); + msg.Body().m_unGiftID = pItem->GetItemID(); + msg.Body().m_ulTargetSteamID = targetID.ConvertToUint64(); + GCClientSystem()->BSendMessage( msg ); + + CSteamID steamID = steamapicontext->SteamUser()->GetSteamID(); + + C_CTF_GameStats.Event_Trading( IE_TRADING_ITEM_GIFTED, pItem, true, + steamID.ConvertToUint64(), targetID.ConvertToUint64() ); +} + +class CDeliverGiftSelectDialog : public CSelectPlayerDialog +{ +public: + CDeliverGiftSelectDialog( vgui::Panel *parent ); + + virtual void ApplySchemeSettings( vgui::IScheme *pScheme ); + void SetItem( CEconItemView* pItem ) { m_pItem = pItem; } + virtual bool AllowOutOfGameFriends() { return true; } + + virtual void OnSelectPlayer( const CSteamID &steamID ) + { + UseGift( m_pItem, steamID ); + } + +private: + CEconItemView* m_pItem; +}; + +CDeliverGiftSelectDialog::CDeliverGiftSelectDialog( vgui::Panel *parent ) +: CSelectPlayerDialog( parent ) +{ +} + +void CDeliverGiftSelectDialog::ApplySchemeSettings( vgui::IScheme *pScheme ) +{ + CSelectPlayerDialog::ApplySchemeSettings( pScheme ); + SetDialogVariable( "title", g_pVGuiLocalize->Find( "TF_DeliverGiftDialog_Title" ) ); +} + +static vgui::DHANDLE<CDeliverGiftSelectDialog> g_hDeliverGiftDialog; + +CDeliverGiftSelectDialog *OpenDeliverGiftDialog( vgui::Panel *pParent, CEconItemView *pItem ) +{ + if (!g_hDeliverGiftDialog.Get()) + { + g_hDeliverGiftDialog = vgui::SETUP_PANEL( new CDeliverGiftSelectDialog( pParent ) ); + } + g_hDeliverGiftDialog->InvalidateLayout( false, true ); + g_hDeliverGiftDialog->Reset(); + g_hDeliverGiftDialog->SetVisible( true ); + g_hDeliverGiftDialog->MakePopup(); + g_hDeliverGiftDialog->MoveToFront(); + g_hDeliverGiftDialog->SetKeyBoardInputEnabled(true); + g_hDeliverGiftDialog->SetMouseInputEnabled(true); + g_hDeliverGiftDialog->SetItem( pItem ); + TFModalStack()->PushModal( g_hDeliverGiftDialog ); + + return g_hDeliverGiftDialog; +} + +// This is the command the user will execute. +// We want this to happen on the client, before forwarding to the game server, since we don't trust +// the game server. +static bool g_bUsedGCItem = false; +static void StartUseActionSlotItem( const CCommand &args ) +{ + if ( !engine->IsInGame() ) + { + return; + } + + C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); + if ( pLocalPlayer == NULL ) + { + return; + } + + pLocalPlayer->SetUsingActionSlot( true ); + + // Ghosts cant use action items! + if ( pLocalPlayer->m_Shared.InCond( TF_COND_HALLOWEEN_GHOST_MODE ) ) + { + return; + } + + // If we're in Mann Vs MAchine, and we're dead, we can use this to respawn instantly. + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() && pLocalPlayer->IsObserver() ) + { + float flNextRespawn = TFGameRules()->GetNextRespawnWave( pLocalPlayer->GetTeamNumber(), pLocalPlayer ); + if ( flNextRespawn ) + { + int iRespawnWait = (flNextRespawn - gpGlobals->curtime); + if ( iRespawnWait > 1.0 ) + { + engine->ClientCmd_Unrestricted( "td_buyback\n" ); + return; + } + } + } + + // trying to pick up a dropped weapon? + if ( pLocalPlayer->GetDroppedWeaponInRange() != NULL ) + { + KeyValues *kv = new KeyValues( "+use_action_slot_item_server" ); + engine->ServerCmdKeyValues( kv ); + return; + } + + if ( TFGameRules() && TFGameRules()->IsUsingGrapplingHook() ) + { + CTFGrapplingHook *pGrapplingHook = dynamic_cast< CTFGrapplingHook* >( pLocalPlayer->GetEntityForLoadoutSlot( LOADOUT_POSITION_ACTION ) ); + if ( pGrapplingHook ) + { + if ( pLocalPlayer->GetActiveTFWeapon() != pGrapplingHook ) + { + pLocalPlayer->Weapon_Switch( pGrapplingHook ); + } + + KeyValues *kv = new KeyValues( "+use_action_slot_item_server" ); + engine->ServerCmdKeyValues( kv ); + + return; + } + } + + // send a request to the GC to use the item + g_bUsedGCItem = false; + CEconItemView *pItem = CTFPlayerSharedUtils::GetEconItemViewByLoadoutSlot( pLocalPlayer, LOADOUT_POSITION_ACTION ); + if( pItem ) + { + const IEconTool *pEconTool = pItem->GetItemDefinition()->GetEconTool(); + bool bIsRecharger = ( pEconTool && FStrEq( pEconTool->GetTypeName(), "item_eater_recharger" ) ); + if ( IsGCUseableItem( pItem->GetItemDefinition() ) && pItem->GetItemQuantity() >= 1 && !bIsRecharger ) + { + UseConsumableItem( pItem, NULL ); + g_bUsedGCItem = true; + } + } + + // otherwise, forward to game server + if ( !g_bUsedGCItem ) + { + KeyValues *kv = new KeyValues( "+use_action_slot_item_server" ); + engine->ServerCmdKeyValues( kv ); + } +} + +static ConCommand start_use_action_slot_item( "+use_action_slot_item", StartUseActionSlotItem, "Use the item in the action slot." ); + +static void EndUseActionSlotItem( const CCommand &args ) +{ + C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); + if ( !pLocalPlayer ) + return; + + pLocalPlayer->SetUsingActionSlot( false ); + + if ( TFGameRules() && TFGameRules()->IsUsingGrapplingHook() && pLocalPlayer->GetActiveTFWeapon() ) + { + // if we're using the hook, switch back to the last weapon + if ( pLocalPlayer->GetActiveTFWeapon()->GetWeaponID() == TF_WEAPON_GRAPPLINGHOOK ) + { + KeyValues *kv = new KeyValues( "-use_action_slot_item_server" ); + engine->ServerCmdKeyValues( kv ); + + C_BaseCombatWeapon* pLastWeapon = pLocalPlayer->GetLastWeapon(); + + // switch away from the hook + if ( pLastWeapon && pLocalPlayer->Weapon_CanSwitchTo( pLastWeapon ) ) + { + pLocalPlayer->Weapon_Switch( pLastWeapon ); + } + else + { + // in case we failed to switch back to last weapon for some reason, just find the next best + pLocalPlayer->SwitchToNextBestWeapon( pLastWeapon ); + } + + return; + } + } + + // tell the game server we let go of the button if this wasn't a GC item + if ( !g_bUsedGCItem ) + { + KeyValues *kv = new KeyValues( "-use_action_slot_item_server" ); + engine->ServerCmdKeyValues( kv ); + } +} + +static ConCommand end_use_action_slot_item( "-use_action_slot_item", EndUseActionSlotItem ); + + +static void StartContextAction( const CCommand &args ) +{ + // Assume we're going to taunt + bool bDoTaunt = true; + + if ( TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) + { + C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); + if ( pLocalPlayer ) + { + CTFPowerupBottle *pPowerupBottle = dynamic_cast< CTFPowerupBottle* >( pLocalPlayer->GetEquippedWearableForLoadoutSlot( LOADOUT_POSITION_ACTION ) ); + if ( pPowerupBottle && pPowerupBottle->GetNumCharges() > 0 ) + { + // They're in MvM and have a bottle with a charge, so do an action instead + bDoTaunt = false; + } + + if ( pLocalPlayer->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) && pLocalPlayer->GetActiveTFWeapon() && pLocalPlayer->GetActiveTFWeapon()->GetWeaponID() == TF_WEAPON_MINIGUN ) + { + int iRage = 0; + CALL_ATTRIB_HOOK_INT_ON_OTHER( pLocalPlayer, iRage, generate_rage_on_dmg ); + if ( iRage ) + { + if ( pLocalPlayer->m_Shared.GetRageMeter() >= 100.f && !pLocalPlayer->m_Shared.IsRageDraining() ) + { + // They have rage ready to go, do the taunt + bDoTaunt = true; + } + } + } + } + } + + if ( bDoTaunt ) + { + // Taunt + engine->ClientCmd_Unrestricted( "+taunt\n" ); + } + else + { + // Action item + StartUseActionSlotItem( args ); + } +} + +static ConCommand start_context_action( "+context_action", StartContextAction, "Use the item in the action slot." ); + +static void EndContextAction( const CCommand &args ) +{ + // Undo both to be on the safe side + EndUseActionSlotItem( args ); + engine->ClientCmd_Unrestricted( "-taunt\n" ); +} + +static ConCommand end_context_action( "-context_action", EndContextAction ); + +//----------------------------------------------------------------------------- + +class CTFGiftNotification : public CEconNotification +{ +public: + CTFGiftNotification( GCSDK::CProtoBufMsg<CMsgGCGiftedItems> &msg ) + : CEconNotification() + { + const EUniverse eUniverse = GetUniverse(); + + Assert( msg.Body().recipient_account_ids_size() > 0 ); + + SetLifetime( 30.0f ); + + m_bRandomPerson = msg.Body().has_was_random_person() + && msg.Body().was_random_person(); + + const CSteamID gifterSteamID( msg.Body().gifter_steam_id(), eUniverse, k_EAccountTypeIndividual ); + SetSteamID( gifterSteamID ); + + if ( m_bRandomPerson ) + { + const CSteamID recipientSteamID( msg.Body().recipient_account_ids(0), eUniverse, k_EAccountTypeIndividual ); + if ( msg.Body().recipient_account_ids_size() > 0 ) + { + // This might not really be a random gift but might instead be a gift they opened + // themselves (ie., a shipment box). + if ( recipientSteamID == gifterSteamID ) + { + SetText( "#TF_GifterText_SelfOpen" ); + } + else + { + SetText( "#TF_GifterText_Random" ); + + char szRecipientName[ MAX_PLAYER_NAME_LENGTH ]; + GetPlayerNameBySteamID( recipientSteamID, szRecipientName, sizeof( szRecipientName ) ); + g_pVGuiLocalize->ConvertANSIToUnicode( szRecipientName, m_wszPlayerName, sizeof( m_wszPlayerName ) ); + AddStringToken( "recipient", m_wszPlayerName ); + m_vecSteamIDRecipients.AddToTail( recipientSteamID ); + } + } + } + else + { + SetText( "#TF_GifterText_All" ); + + for ( int i = 0; i < msg.Body().recipient_account_ids_size(); ++i ) + { + const CSteamID recipientSteamID( msg.Body().recipient_account_ids(i), eUniverse, k_EAccountTypeIndividual ); + m_vecSteamIDRecipients.AddToTail( recipientSteamID ); + } + } + + char szGifterName[ MAX_PLAYER_NAME_LENGTH ]; + GetPlayerNameBySteamID( m_steamID, szGifterName, sizeof( szGifterName ) ); + g_pVGuiLocalize->ConvertANSIToUnicode( szGifterName, m_wszPlayerName, sizeof( m_wszPlayerName ) ); + AddStringToken( "giver", m_wszPlayerName ); + + PrintToChatLog(); + + SetSoundFilename( "misc/happy_birthday.wav" ); + } + + void PrintToChatLog() + { + CBaseHudChat *pHUDChat = (CBaseHudChat *)GET_HUDELEMENT( CHudChat ); + if ( pHUDChat ) + { + wchar_t *pFormat = g_pVGuiLocalize->Find( "TF_GiftedItems" ); + if ( pFormat == NULL ) + { + return; + } + FOR_EACH_VEC( m_vecSteamIDRecipients, i ) + { + const CSteamID &steamIDRecipient = m_vecSteamIDRecipients[i]; + char szRecipientName[ MAX_PLAYER_NAME_LENGTH ]; + szRecipientName[0] = '\0'; + GetPlayerNameBySteamID( steamIDRecipient, szRecipientName, sizeof( szRecipientName ) ); + if ( szRecipientName[0] == '\0' ) + { + continue; + } + + wchar_t wszRecipientName[MAX_PLAYER_NAME_LENGTH]; + g_pVGuiLocalize->ConvertANSIToUnicode( szRecipientName, wszRecipientName, sizeof( wszRecipientName ) ); + + wchar_t wszNotification[1024]=L""; + g_pVGuiLocalize->ConstructString_safe( wszNotification, + pFormat, + 2, m_wszPlayerName, wszRecipientName ); + + char szAnsi[1024]; + g_pVGuiLocalize->ConvertUnicodeToANSI( wszNotification, szAnsi, sizeof(szAnsi) ); + + pHUDChat->Printf( CHAT_FILTER_NONE, "%s", szAnsi ); + } + } + } + + virtual EType NotificationType() { return eType_Basic; } + + wchar_t m_wszPlayerName[ MAX_PLAYER_NAME_LENGTH ]; + bool m_bRandomPerson; + CUtlVector< CSteamID > m_vecSteamIDRecipients; +}; + +//#ifdef _DEBUG +//CON_COMMAND( cl_gifts_test, "tests the gift ui." ) +//{ +// if ( !engine->IsInGame() ) +// { +// return; +// } +// +// C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); +// if ( pLocalPlayer == NULL ) +// { +// return; +// } +// +// if ( steamapicontext == NULL || steamapicontext->SteamUser() == NULL ) +// { +// return; +// } +// +// CSteamID steamID = steamapicontext->SteamUser()->GetSteamID(); +// GCSDK::CProtoBufMsg< CMsgGCGiftedItems > msg( k_EMsgGCGiftedItems ); +// msg.Body().m_ulGifterSteamID = steamID.ConvertToUint64(); +// msg.Body().m_bRandomPerson = ( args.ArgC() >= 2 ); +// msg.Body().m_unNumGiftRecipients = msg.Body().m_bRandomPerson ? 1 : 31; +// for ( int i = 0; i < msg.Body().m_unNumGiftRecipients; ++i ) +// { +// msg.AddUint64Data( steamID.ConvertToUint64() ); +// } +// msg.ResetReadPtr(); +// NotificationQueue_Add( new CTFGiftNotification( msg ) ); +//} +//#endif + +//----------------------------------------------------------------------------- +// Purpose: Feedback to the local player who used an item +//----------------------------------------------------------------------------- +class CTFUseItemNotification : public CEconNotification +{ +public: + CTFUseItemNotification( EGCMsgUseItemResponse eResponse) + : CEconNotification() + { + switch ( eResponse ) + { + case k_EGCMsgUseItemResponse_ItemUsed: + SetText( "#TF_UseItem_Success" ); + break; + case k_EGCMsgUseItemResponse_GiftNoOtherPlayers: + SetText( "#TF_UseItem_GiftNoPlayers" ); + break; + case k_EGCMsgUseItemResponse_ServerError: + SetText( "#TF_UseItem_Error" ); + break; + case k_EGCMsgUseItemResponse_MiniGameAlreadyStarted: + SetText( "#TF_UseItem_MiniGameAlreadyStarted" ); + break; + case k_EGCMsgUseItemResponse_CannotBeUsedByAccount: + SetText( "#TF_UseItem_CannotBeUsedByAccount" ); + break; + default: + Assert( !"Unknown response in CTFUseItemNotification!" ); + } + SetLifetime( 20.0f ); + } + + virtual EType NotificationType() { return eType_Basic; } +}; + +//----------------------------------------------------------------------------- +// Purpose: Local player used an item and the GC responded with the status of that request +//----------------------------------------------------------------------------- +class CGCUseItemResponse : public GCSDK::CGCClientJob +{ +public: + CGCUseItemResponse( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} + virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) + { + GCSDK::CGCMsg<MsgGCUseItemResponse_t> msg( pNetPacket ); + EGCMsgUseItemResponse eResponse = (EGCMsgUseItemResponse)msg.Body().m_eResponse; + if ( eResponse == k_EGCMsgUseItemResponse_ItemUsed_ItemsGranted ) + { + if ( s_bConsumableToolOpeningGift ) + { + vgui::surface()->PlaySound( "ui/item_gift_wrap_unwrap.wav" ); + } + else + { + vgui::surface()->PlaySound( "ui/item_open_crate.wav" ); + } + + ShowWaitingDialog( new CWaitForPackageDialog( NULL ), "#ToolDecodeInProgress", true, false, 5.0f ); + } + else if ( eResponse != k_EGCMsgUseItemResponse_ItemUsed ) + { + NotificationQueue_Add( new CTFUseItemNotification( (EGCMsgUseItemResponse)msg.Body().m_eResponse ) ); + } + else + { + // refresh the backpack + if ( EconUI()->GetBackpackPanel() ) + { + EconUI()->GetBackpackPanel()->UpdateModelPanels(); + } + } + return true; + } +}; +GC_REG_JOB( GCSDK::CGCClient, CGCUseItemResponse, "CGCUseItemResponse", k_EMsgGCUseItemResponse, GCSDK::k_EServerTypeGCClient ); + +//----------------------------------------------------------------------------- +// Purpose: A player has gifted items +//----------------------------------------------------------------------------- +class CGCGiftedItems : public GCSDK::CGCClientJob +{ +public: + CGCGiftedItems( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} + virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) + { + GCSDK::CProtoBufMsg<CMsgGCGiftedItems> msg( pNetPacket ); + + if ( steamapicontext == NULL || steamapicontext->SteamFriends() == NULL ) + { + return true; + } + + char szGifterName[ MAX_PLAYER_NAME_LENGTH ]; + szGifterName[0] = '\0'; + GetPlayerNameBySteamID( CSteamID( msg.Body().gifter_steam_id(), GetUniverse(), k_EAccountTypeIndividual ), szGifterName, sizeof( szGifterName ) ); + if ( szGifterName[0] == '\0' ) + { + return true; + } + + // notify UI + NotificationQueue_Add( new CTFGiftNotification( msg ) ); + return true; + } +}; +GC_REG_JOB( GCSDK::CGCClient, CGCGiftedItems, "CGCGiftedItems", k_EMsgGCGiftedItems, GCSDK::k_EServerTypeGCClient ); + +//----------------------------------------------------------------------------- +// Purpose: A player has used a claim code item +//----------------------------------------------------------------------------- +class CGCUsedClaimCodeItem : public GCSDK::CGCClientJob +{ +public: + CGCUsedClaimCodeItem( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} + virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) + { + GCSDK::CGCMsg<MsgGCUsedClaimCodeItem_t> msg( pNetPacket ); + + if ( steamapicontext == NULL ) + { + return true; + } + + CUtlString url; + if ( msg.BReadStr( &url ) ) + { + steamapicontext->SteamFriends()->ActivateGameOverlayToWebPage( url.Get() ); + IViewPortPanel *pMMOverride = ( gViewPortInterface->FindPanelByName( PANEL_MAINMENUOVERRIDE ) ); + if ( pMMOverride ) + { + ((CHudMainMenuOverride*)pMMOverride)->UpdatePromotionalCodes(); + } + } + + return true; + } +}; +GC_REG_JOB( GCSDK::CGCClient, CGCUsedClaimCodeItem, "CGCUsedClaimCodeItem", k_EMsgGCUsedClaimCodeItem, GCSDK::k_EServerTypeGCClient ); + + +//----------------------------------------------------------------------------- +// Duel mini-game +//----------------------------------------------------------------------------- + +class CDuelMiniGameEventListener; + +struct duel_minigame_local_data_t +{ + duel_minigame_local_data_t() + : m_pEventListener( NULL ) + , m_steamIDOpponent() + , m_unMyScore( 0 ) + , m_unOpponentScore( 0 ) + , m_iRequiredPlayerClass( TF_CLASS_UNDEFINED ) + { + } + uint32 m_unMyScore; + uint32 m_unOpponentScore; + int m_iRequiredPlayerClass; + CDuelMiniGameEventListener *m_pEventListener; + CSteamID m_steamIDOpponent; +}; +static duel_minigame_local_data_t gDuelMiniGameLocalData; + +bool DuelMiniGame_IsDueling() +{ + return gDuelMiniGameLocalData.m_steamIDOpponent != CSteamID(); +} + +int DuelMiniGame_GetRequiredPlayerClass() +{ + return gDuelMiniGameLocalData.m_steamIDOpponent != CSteamID() ? gDuelMiniGameLocalData.m_iRequiredPlayerClass : TF_CLASS_UNDEFINED; +} + +static void DuelMiniGame_Reset(); +static bool RemoveRelatedDuelNotifications( CEconNotification* pNotification ); + +/** + * Duel info notification + */ +class CTFDuelInfoNotification : public CEconNotification +{ +public: + CTFDuelInfoNotification() + { + } + + static bool IsDuelInfoNotification( CEconNotification *pNotification ) + { + return dynamic_cast< CTFDuelInfoNotification* >( pNotification ) != NULL; + } +}; + +/** + * Duel Notification + */ +class CTFDuelRequestNotification : public CEconNotification, public CGameEventListener +{ +public: + CTFDuelRequestNotification( const char *pInitiatorName, const CSteamID &steamIDInitiator, const CSteamID &steamIDTarget, const int iRequiredPlayerClass ) + : CEconNotification() + , m_steamIDInitiator( steamIDInitiator ) + , m_steamIDTarget( steamIDTarget ) + , m_iRequiredPlayerClass( iRequiredPlayerClass ) + { + g_pVGuiLocalize->ConvertANSIToUnicode( pInitiatorName, m_wszPlayerName, sizeof(m_wszPlayerName) ); + + ListenForGameEvent( "teamplay_round_win" ); + ListenForGameEvent( "teamplay_round_stalemate" ); + } + + virtual EType NotificationType() + { + CSteamID localSteamID; + C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); + if ( pLocalPlayer && pLocalPlayer->GetSteamID( &localSteamID ) && localSteamID == m_steamIDTarget ) + { + return eType_AcceptDecline; + } + return eType_Basic; + } + + // XXX(JohnS): Is there something that manually calls trigger here or is this dead code? + virtual void Trigger() + { + CTFGenericConfirmDialog *pDialog = ShowConfirmDialog( "#TF_Duel_Title", "#TF_Duel_Request", "#GameUI_OK", "#TF_Duel_JoinCancel", &ConfirmDuel ); + pDialog->SetContext( this ); + pDialog->AddStringToken( "initiator", m_wszPlayerName ); + // so we aren't deleted + SetIsInUse( true ); + } + + virtual void Accept() + { + ConfirmDuel( true, this ); + } + + virtual void Decline() + { + ConfirmDuel( false, this ); + } + + static bool IsDuelRequestNotification( CEconNotification *pNotification ) + { + return dynamic_cast< CTFDuelRequestNotification* >( pNotification ) != NULL; + } + + static void ConfirmDuel( bool bConfirmed, void *pContext ) + { + CTFDuelRequestNotification *pNotification = (CTFDuelRequestNotification*)pContext; + + // if this is a class restricted duel, then make sure the local player is the same class before + // they can accept + C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); + if ( bConfirmed && pLocalPlayer && pNotification->m_iRequiredPlayerClass != TF_CLASS_UNDEFINED ) + { + int iClass = pLocalPlayer->m_Shared.GetDesiredPlayerClassIndex(); + if ( pNotification->m_iRequiredPlayerClass != iClass ) + { + KeyValues *pKeyValues = new KeyValues( "DuelConfirm" ); + switch ( pNotification->m_iRequiredPlayerClass ) + { + case TF_CLASS_SCOUT: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Scout" ) ); break; + case TF_CLASS_SNIPER: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Sniper" ) ); break; + case TF_CLASS_SOLDIER: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Soldier" ) ); break; + case TF_CLASS_DEMOMAN: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Demoman" ) ); break; + case TF_CLASS_MEDIC: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Medic" ) ); break; + case TF_CLASS_HEAVYWEAPONS: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_HWGuy" ) ); break; + case TF_CLASS_PYRO: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Pyro" ) ); break; + case TF_CLASS_SPY: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Spy" ) ); break; + case TF_CLASS_ENGINEER: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Engineer" ) ); break; + } + ShowMessageBox( "#TF_Duel_Title", "#TF_Duel_WrongClass", pKeyValues, "#GameUI_OK" ); + return; + } + } + + // notify GC of our choice + GCSDK::CGCMsg< MsgGC_Duel_Response_t > msg( k_EMsgGC_Duel_Response ); + msg.Body().m_ulInitiatorSteamID = pNotification->m_steamIDInitiator.ConvertToUint64(); + msg.Body().m_ulTargetSteamID = pNotification->m_steamIDTarget.ConvertToUint64(); + msg.Body().m_bAccepted = bConfirmed; + GCClientSystem()->BSendMessage( msg ); + pNotification->SetIsInUse( false ); + pNotification->MarkForDeletion(); + + // remove all duel notifications if we've accepted + if ( bConfirmed ) + { + NotificationQueue_Remove( &RemoveRelatedDuelNotifications ); + } + } + + void FireGameEvent( IGameEvent *event ) + { + const char *pEventName = event->GetName(); + if ( FStrEq( "teamplay_round_win", pEventName ) || FStrEq( "teamplay_round_stalemate", pEventName ) ) + { + Decline(); + return; + } + } + + CSteamID m_steamIDInitiator; + CSteamID m_steamIDTarget; + int m_iRequiredPlayerClass; + wchar_t m_wszPlayerName[MAX_PLAYER_NAME_LENGTH]; +}; + +/** + * Listens for duel_status events and adds a notification. Ideally adds to a scoreboard or something... + */ +class CDuelMiniGameEventListener : public CGameEventListener +{ +public: + CDuelMiniGameEventListener() + { + ListenForGameEvent( "duel_status" ); + ListenForGameEvent( "teamplay_round_win" ); + ListenForGameEvent( "teamplay_round_stalemate" ); + } + + virtual void FireGameEvent( IGameEvent *event ) + { + const char *pEventName = event->GetName(); + + C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); + if ( pLocalPlayer == NULL ) + { + return; + } + + if ( Q_strcmp( "teamplay_round_win", pEventName ) == 0 || Q_strcmp( "teamplay_round_stalemate", pEventName ) == 0 ) + { + DuelMiniGame_Reset(); + return; + } + else if ( Q_strcmp( "duel_status", pEventName ) == 0 ) + { + int iKillerID = engine->GetPlayerForUserID( event->GetInt( "killer" ) ); + int iInitiatorID = engine->GetPlayerForUserID( event->GetInt( "initiator" ) ); + int iTargetID = engine->GetPlayerForUserID( event->GetInt( "target" ) ); + + const char *pInitiatorName = ( iInitiatorID > 0 ? g_PR->GetPlayerName( iInitiatorID ) : "" ); + wchar_t wszInitiatorName[MAX_PLAYER_NAME_LENGTH] = L""; + g_pVGuiLocalize->ConvertANSIToUnicode( pInitiatorName, wszInitiatorName, sizeof(wszInitiatorName) ); + + const char *pTargetName = ( iTargetID > 0 ? g_PR->GetPlayerName( iTargetID ) : "" ); + wchar_t wszTargetName[MAX_PLAYER_NAME_LENGTH] = L""; + g_pVGuiLocalize->ConvertANSIToUnicode( pTargetName, wszTargetName, sizeof(wszTargetName) ); + + wchar_t wszInitiatorScore[16]; + _snwprintf( wszInitiatorScore, ARRAYSIZE( wszInitiatorScore ), L"%i", event->GetInt( "initiator_score", 0 ) ); + wchar_t wszTargetScore[16]; + _snwprintf( wszTargetScore, ARRAYSIZE( wszTargetScore ), L"%i", event->GetInt( "target_score", 0 ) ); + + enum + { + kDuelScoreType_Kill, + kDuelScoreType_Assist, + kMaxDuelScoreTypes, + }; + + int iScoreType = event->GetInt( "score_type" ); + + KeyValues *pKeyValues = new KeyValues( "DuelStatus" ); + pKeyValues->SetWString( "killer", iKillerID == iInitiatorID ? wszInitiatorName : wszTargetName ); + pKeyValues->SetWString( "initiator", wszInitiatorName ); + pKeyValues->SetWString( "target", wszTargetName ); + pKeyValues->SetWString( "initiator_score", wszInitiatorScore ); + pKeyValues->SetWString( "target_score", wszTargetScore ); + + // if we aren't involved in the duel, don't show the notification + if ( pLocalPlayer->entindex() == iInitiatorID || pLocalPlayer->entindex() == iTargetID ) + { + // remove existing duel info notifications + NotificationQueue_Remove( &RemoveRelatedDuelNotifications ); + // add new one + CTFDuelInfoNotification *pNotification = new CTFDuelInfoNotification(); + pNotification->SetLifetime( 10.0f ); + pNotification->SetText( iScoreType == kDuelScoreType_Kill ? "TF_Duel_StatusKill" : "TF_Duel_StatusAssist" ); + pNotification->SetKeyValues( pKeyValues ); + pNotification->SetSoundFilename( "ui/duel_event.wav" ); + player_info_t pi; + if ( engine->GetPlayerInfo( iKillerID, &pi ) && pi.friendsID != 0 ) + { + CSteamID steamID( pi.friendsID, 1, GetUniverse(), k_EAccountTypeIndividual ); + pNotification->SetSteamID( steamID ); + } + NotificationQueue_Add( pNotification ); + if ( pLocalPlayer->entindex() == iInitiatorID ) + { + gDuelMiniGameLocalData.m_unMyScore = event->GetInt( "initiator_score" ); + gDuelMiniGameLocalData.m_unOpponentScore = event->GetInt( "target_score" ); + } + else + { + gDuelMiniGameLocalData.m_unMyScore = event->GetInt( "target_score" ); + gDuelMiniGameLocalData.m_unOpponentScore = event->GetInt( "initiator_score" ); + } + } + + // print to chat log + PrintTextToChat( iScoreType == kDuelScoreType_Kill ? "TF_Duel_StatusForChat_Kill" : "TF_Duel_StatusForChat_Assist", pKeyValues ); + + // cleanup + pKeyValues->deleteThis(); + } + } +}; + +/** + * Duel request + */ +class CGC_Duel_Request : public GCSDK::CGCClientJob +{ +public: + CGC_Duel_Request( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} + virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) + { + GCSDK::CGCMsg<MsgGC_Duel_Request_t> msg( pNetPacket ); + CSteamID localSteamID; + C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); + if ( pLocalPlayer == NULL || pLocalPlayer->GetSteamID( &localSteamID ) == false ) + { + return true; + } + + // get player names + CSteamID steamIDInitiator( msg.Body().m_ulInitiatorSteamID ); + CSteamID steamIDTarget( msg.Body().m_ulTargetSteamID ); + + // ignore blocked players (we don't want to print out to the console either) + if ( IgnoreRequestFromUser( steamIDInitiator ) || IgnoreRequestFromUser( steamIDTarget ) ) + { + GCSDK::CGCMsg< MsgGC_Duel_Response_t > msgGC( k_EMsgGC_Duel_Response ); + msgGC.Body().m_ulInitiatorSteamID = steamIDInitiator.ConvertToUint64(); + msgGC.Body().m_ulTargetSteamID = steamIDTarget.ConvertToUint64(); + msgGC.Body().m_bAccepted = false; + GCClientSystem()->BSendMessage( msgGC ); + return true; + } + + char szPlayerName_Initiator[ MAX_PLAYER_NAME_LENGTH ]; + GetPlayerNameBySteamID( steamIDInitiator, szPlayerName_Initiator, sizeof( szPlayerName_Initiator ) ); + char szPlayerName_Target[ MAX_PLAYER_NAME_LENGTH ]; + GetPlayerNameBySteamID( steamIDTarget, szPlayerName_Target, sizeof( szPlayerName_Target ) ); + wchar_t wszPlayerName_Initiator[MAX_PLAYER_NAME_LENGTH]; + wchar_t wszPlayerName_Target[MAX_PLAYER_NAME_LENGTH]; + g_pVGuiLocalize->ConvertANSIToUnicode( szPlayerName_Initiator, wszPlayerName_Initiator, sizeof(wszPlayerName_Initiator) ); + g_pVGuiLocalize->ConvertANSIToUnicode( szPlayerName_Target, wszPlayerName_Target, sizeof(wszPlayerName_Target) ); + + // add notification + KeyValues *pKeyValues = new KeyValues( "DuelText" ); + pKeyValues->SetWString( "initiator", wszPlayerName_Initiator ); + pKeyValues->SetWString( "target", wszPlayerName_Target ); + + const char *pText = localSteamID == steamIDTarget ? "TF_Duel_Request" : "TF_Duel_Challenge"; + const char *pSoundFilename = "ui/duel_challenge.wav"; + if ( msg.Body().m_usAsPlayerClass >= TF_FIRST_NORMAL_CLASS && msg.Body().m_usAsPlayerClass < TF_LAST_NORMAL_CLASS ) + { + pText = localSteamID == steamIDTarget ? "TF_Duel_Request_Class" : "TF_Duel_Challenge_Class"; + pSoundFilename = "ui/duel_challenge_with_restriction.wav"; + switch ( msg.Body().m_usAsPlayerClass ) + { + case TF_CLASS_SCOUT: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Scout" ) ); break; + case TF_CLASS_SNIPER: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Sniper" ) ); break; + case TF_CLASS_SOLDIER: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Soldier" ) ); break; + case TF_CLASS_DEMOMAN: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Demoman" ) ); break; + case TF_CLASS_MEDIC: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Medic" ) ); break; + case TF_CLASS_HEAVYWEAPONS: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_HWGuy" ) ); break; + case TF_CLASS_PYRO: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Pyro" ) ); break; + case TF_CLASS_SPY: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Spy" ) ); break; + case TF_CLASS_ENGINEER: pKeyValues->SetWString( "player_class", g_pVGuiLocalize->Find( "#TF_Class_Name_Engineer" ) ); break; + } + } + if ( localSteamID == steamIDInitiator || localSteamID == steamIDTarget ) + { + CTFDuelRequestNotification *pNotification = new CTFDuelRequestNotification( szPlayerName_Initiator, steamIDInitiator, steamIDTarget, msg.Body().m_usAsPlayerClass ); + pNotification->SetLifetime( localSteamID == steamIDTarget ? 30.0f : 7.0f ); + pNotification->SetText( pText ); + pNotification->SetKeyValues( pKeyValues ); + pNotification->SetSteamID( steamIDInitiator ); + pNotification->SetSoundFilename( pSoundFilename ); + NotificationQueue_Add( pNotification ); + } + + // print to chat log + PrintTextToChat( pText, pKeyValues ); + + pKeyValues->deleteThis(); + + return true; + } +protected: +}; +GC_REG_JOB( GCSDK::CGCClient, CGC_Duel_Request, "CGC_Duel_Request", k_EMsgGC_Duel_Request, GCSDK::k_EServerTypeGCClient ); + +/** + * Duel response + */ +class CGCClient_Duel_Response : public GCSDK::CGCClientJob +{ +public: + CGCClient_Duel_Response( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} + virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) + { + GCSDK::CGCMsg<MsgGC_Duel_Response_t> msg( pNetPacket ); + CSteamID localSteamID; + C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); + if ( pLocalPlayer == NULL || pLocalPlayer->GetSteamID( &localSteamID ) == false ) + return true; + + // get player names + CSteamID steamIDInitiator( msg.Body().m_ulInitiatorSteamID ); + CSteamID steamIDTarget( msg.Body().m_ulTargetSteamID ); + char szPlayerName_Initiator[ MAX_PLAYER_NAME_LENGTH ]; + GetPlayerNameBySteamID( steamIDInitiator, szPlayerName_Initiator, sizeof( szPlayerName_Initiator ) ); + char szPlayerName_Target[ MAX_PLAYER_NAME_LENGTH ]; + GetPlayerNameBySteamID( steamIDTarget, szPlayerName_Target, sizeof( szPlayerName_Target ) ); + wchar_t wszPlayerName_Initiator[MAX_PLAYER_NAME_LENGTH]; + wchar_t wszPlayerName_Target[MAX_PLAYER_NAME_LENGTH]; + g_pVGuiLocalize->ConvertANSIToUnicode( szPlayerName_Initiator, wszPlayerName_Initiator, sizeof(wszPlayerName_Initiator) ); + g_pVGuiLocalize->ConvertANSIToUnicode( szPlayerName_Target, wszPlayerName_Target, sizeof(wszPlayerName_Target) ); + + KeyValues *pKeyValues = new KeyValues( "DuelText" ); + pKeyValues->SetWString( "initiator", wszPlayerName_Initiator ); + pKeyValues->SetWString( "target", wszPlayerName_Target ); + + bool bIsClassDuel = msg.Body().m_usAsPlayerClass >= TF_FIRST_NORMAL_CLASS && msg.Body().m_usAsPlayerClass < TF_LAST_NORMAL_CLASS; + + // add notification + const char *pText = "TF_Duel_Accept"; + const char *pSoundFilename = bIsClassDuel ? "ui/duel_challenge_accepted_with_restriction.wav" : "ui/duel_challenge_accepted.wav"; + if ( msg.Body().m_bAccepted == false ) + { + const char *kDeclineStrings[] = { + "TF_Duel_Decline", + "TF_Duel_Decline2", + "TF_Duel_Decline3" + }; + pText = kDeclineStrings[ RandomInt( 0, ARRAYSIZE( kDeclineStrings ) - 1 ) ]; + pSoundFilename = bIsClassDuel ? "ui/duel_challenge_rejected_with_restriction.wav" : "ui/duel_challenge_rejected.wav"; + } + + if ( ( TFGameRules() && TFGameRules()->CanInitiateDuels() ) || msg.Body().m_bAccepted ) + { + if ( localSteamID == steamIDInitiator || localSteamID == steamIDTarget ) + { + // remove existing duel info notifications + NotificationQueue_Remove( &RemoveRelatedDuelNotifications ); + // add new one + CTFDuelInfoNotification *pNotification = new CTFDuelInfoNotification(); + pNotification->SetLifetime( 7.0f ); + pNotification->SetText( pText ); + pNotification->SetKeyValues( pKeyValues ); + pNotification->SetSteamID( steamIDInitiator ); + pNotification->SetSoundFilename( pSoundFilename ); + NotificationQueue_Add( pNotification ); + } + + // print to chat + PrintTextToChat( pText, pKeyValues ); + } + + // store away opponent id and create event listener if applicable + if ( msg.Body().m_bAccepted ) + { + if ( localSteamID == steamIDInitiator ) + { + gDuelMiniGameLocalData.m_steamIDOpponent = steamIDTarget; + } + else if ( localSteamID == steamIDTarget ) + { + gDuelMiniGameLocalData.m_steamIDOpponent = steamIDInitiator; + } + if ( gDuelMiniGameLocalData.m_pEventListener == NULL ) + { + gDuelMiniGameLocalData.m_pEventListener = new CDuelMiniGameEventListener(); + } + gDuelMiniGameLocalData.m_iRequiredPlayerClass = msg.Body().m_usAsPlayerClass; + } + + pKeyValues->deleteThis(); + + return true; + } +protected: +}; +GC_REG_JOB( GCSDK::CGCClient, CGCClient_Duel_Response, "CGCClient_Duel_Response", k_EMsgGC_Duel_Response, GCSDK::k_EServerTypeGCClient ); + +/** + * Duel status (whether the duel is in progress or cancelled) + */ +class CGC_Duel_Status : public GCSDK::CGCClientJob +{ +public: + CGC_Duel_Status( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} + virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) + { + GCSDK::CGCMsg<MsgGC_Duel_Status_t> msg( pNetPacket ); + // get player names + CSteamID steamIDInitiator( msg.Body().m_ulInitiatorSteamID ); + CSteamID steamIDTarget( msg.Body().m_ulTargetSteamID ); + char szPlayerName_Initiator[ MAX_PLAYER_NAME_LENGTH ]; + GetPlayerNameBySteamID( steamIDInitiator, szPlayerName_Initiator, sizeof( szPlayerName_Initiator ) ); + char szPlayerName_Target[ MAX_PLAYER_NAME_LENGTH ]; + GetPlayerNameBySteamID( steamIDTarget, szPlayerName_Target, sizeof( szPlayerName_Target ) ); + wchar_t wszPlayerName_Initiator[MAX_PLAYER_NAME_LENGTH]; + wchar_t wszPlayerName_Target[MAX_PLAYER_NAME_LENGTH]; + g_pVGuiLocalize->ConvertANSIToUnicode( szPlayerName_Initiator, wszPlayerName_Initiator, sizeof(wszPlayerName_Initiator) ); + g_pVGuiLocalize->ConvertANSIToUnicode( szPlayerName_Target, wszPlayerName_Target, sizeof(wszPlayerName_Target) ); + + // add notification + const char *pText = "TF_Duel_InProgress"; + switch ( msg.Body().m_usStatus ) + { + case kDuel_Status_AlreadyInDuel_Inititator: + pText = "TF_Duel_InADuel_Initiator"; + break; + case kDuel_Status_AlreadyInDuel_Target: + pText = "TF_Duel_InADuel_Target"; + break; + case kDuel_Status_DuelBanned_Initiator: + pText = "TF_Duel_TempBanned_Initiator"; + break; + case kDuel_Status_DuelBanned_Target: + pText = "TF_Duel_TempBanned_Target"; + break; + case kDuel_Status_Cancelled: + pText = "TF_Duel_Cancelled"; + break; + case kDuel_Status_MissingSession: + pText = "TF_Duel_UserTemporarilyUnavailable"; + break; + default: + AssertMsg1( false, "Unknown duel status %i in CGC_Duel_Status!", msg.Body().m_usStatus ); + } + // remove existing duel info notifications + NotificationQueue_Remove( &RemoveRelatedDuelNotifications ); + // add new one + CTFDuelInfoNotification *pNotification = new CTFDuelInfoNotification(); + pNotification->SetLifetime( 7.0f ); + pNotification->SetText( pText ); + pNotification->AddStringToken( "initiator", wszPlayerName_Initiator ); + pNotification->AddStringToken( "target", wszPlayerName_Target ); + pNotification->SetSteamID( steamIDInitiator ); + pNotification->SetSoundFilename( "ui/duel_event.wav" ); + NotificationQueue_Add( pNotification ); + return true; + } +protected: +}; +GC_REG_JOB( GCSDK::CGCClient, CGC_Duel_Status, "CGC_Duel_Status", k_EMsgGC_Duel_Status, GCSDK::k_EServerTypeGCClient ); + +/** + * Duel Results--ideally this is a scoreboard as well. + */ +class CGC_Duel_Results : public GCSDK::CGCClientJob +{ +public: + CGC_Duel_Results( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} + virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) + { + GCSDK::CGCMsg<MsgGC_Duel_Results_t> msg( pNetPacket ); + + if ( steamapicontext == NULL || steamapicontext->SteamUser() == NULL ) + return true; + + CSteamID localSteamID = steamapicontext->SteamUser()->GetSteamID(); + + // get player names + CSteamID steamIDWinner( msg.Body().m_ulWinnerSteamID ); + CSteamID steamIDInitiator( msg.Body().m_ulInitiatorSteamID ); + CSteamID steamIDTarget( msg.Body().m_ulTargetSteamID ); + char szPlayerName_Winner[ MAX_PLAYER_NAME_LENGTH ]; + GetPlayerNameBySteamID( steamIDWinner, szPlayerName_Winner, sizeof( szPlayerName_Winner ) ); + char szPlayerName_Initiator[ MAX_PLAYER_NAME_LENGTH ]; + GetPlayerNameBySteamID( steamIDInitiator, szPlayerName_Initiator, sizeof( szPlayerName_Initiator ) ); + char szPlayerName_Target[ MAX_PLAYER_NAME_LENGTH ]; + GetPlayerNameBySteamID( steamIDTarget, szPlayerName_Target, sizeof( szPlayerName_Target ) ); + wchar_t wszPlayerName_Winner[MAX_PLAYER_NAME_LENGTH]; + wchar_t wszPlayerName_Initiator[MAX_PLAYER_NAME_LENGTH]; + wchar_t wszPlayerName_Target[MAX_PLAYER_NAME_LENGTH]; + g_pVGuiLocalize->ConvertANSIToUnicode( szPlayerName_Winner, wszPlayerName_Winner, sizeof(wszPlayerName_Winner) ); + g_pVGuiLocalize->ConvertANSIToUnicode( szPlayerName_Initiator, wszPlayerName_Initiator, sizeof(wszPlayerName_Initiator) ); + g_pVGuiLocalize->ConvertANSIToUnicode( szPlayerName_Target, wszPlayerName_Target, sizeof(wszPlayerName_Target) ); + + // build text + bool bTie = msg.Body().m_usScoreInitiator == msg.Body().m_usScoreTarget; + const char *pText = "TF_Duel_Win"; + switch ( msg.Body().m_usEndReason ) + { + case kDuelEndReason_DuelOver: + break; + case kDuelEndReason_PlayerDisconnected: + bTie = false; + pText = "TF_Duel_Win_Disconnect"; + break; + case kDuelEndReason_PlayerSwappedTeams: + bTie = false; + pText = "TF_Duel_Win_SwappedTeams"; + break; + case kDuelEndReason_LevelShutdown: + bTie = true; + pText = "TF_Duel_Refund_LevelShutdown"; + break; + case kDuelEndReason_ScoreTiedAtZero: + bTie = true; + pText = "TF_Duel_Refund_ScoreTiedAtZero"; + break; + case kDuelEndReason_ScoreTied: + bTie = true; + pText = "TF_Duel_Tie"; + break; + case kDuelEndReason_PlayerKicked: + bTie = true; + pText = "TF_Duel_Refund_Kicked"; + break; + case kDuelEndReason_PlayerForceSwappedTeams: + bTie = true; + pText = "TF_Duel_Refund_ForceTeamSwap"; + break; + case kDuelEndReason_Cancelled: + bTie = true; + pText = "TF_Duel_Cancelled"; + break; + } + + KeyValues *pKeyValues = new KeyValues( "DuelResults" ); + if ( bTie == false ) + { + pKeyValues->SetWString( "winner", wszPlayerName_Winner ); + pKeyValues->SetWString( "loser", steamIDWinner == steamIDInitiator ? wszPlayerName_Target : wszPlayerName_Initiator ); + wchar_t wszScoreInitiator[16]; + wchar_t wszScoreTarget[16]; + _snwprintf( wszScoreInitiator, ARRAYSIZE( wszScoreInitiator ), L"%u", (uint32)msg.Body().m_usScoreInitiator ); + _snwprintf( wszScoreTarget, ARRAYSIZE( wszScoreTarget ), L"%u", (uint32)msg.Body().m_usScoreTarget ); + pKeyValues->SetWString( "winner_score", steamIDWinner == steamIDInitiator ? wszScoreInitiator : wszScoreTarget ); + pKeyValues->SetWString( "loser_score", steamIDWinner == steamIDInitiator ? wszScoreTarget : wszScoreInitiator ); + } + else + { + wchar_t wszScoreInitiator[16]; + _snwprintf( wszScoreInitiator, ARRAYSIZE( wszScoreInitiator ), L"%u", (uint32)msg.Body().m_usScoreInitiator ); + pKeyValues->SetWString( "score", wszScoreInitiator ); + pKeyValues->SetWString( "initiator", wszPlayerName_Initiator ); + pKeyValues->SetWString( "target", wszPlayerName_Target ); + } + + // add notification + if ( localSteamID == steamIDInitiator || localSteamID == steamIDTarget ) + { + // remove existing duel info notifications + NotificationQueue_Remove( &RemoveRelatedDuelNotifications ); + // add new one + CTFDuelInfoNotification *pNotification = new CTFDuelInfoNotification(); + pNotification->SetText( pText ); + pNotification->SetKeyValues( pKeyValues ); + pNotification->SetSteamID( bTie == false ? steamIDWinner : ( localSteamID == steamIDInitiator ? steamIDTarget : steamIDInitiator ) ); + pNotification->SetSoundFilename( "ui/duel_event.wav" ); + NotificationQueue_Add( pNotification ); + gDuelMiniGameLocalData.m_steamIDOpponent = CSteamID(); + } + + // print out to chat + PrintTextToChat( pText, pKeyValues ); + + // cleanup + pKeyValues->deleteThis(); + + // reset the dueling minigame + DuelMiniGame_Reset(); + + return true; + } +protected: +}; +GC_REG_JOB( GCSDK::CGCClient, CGC_Duel_Results, "CGC_Duel_Results", k_EMsgGC_Duel_Results, GCSDK::k_EServerTypeGCClient ); + +static bool RemoveRelatedDuelNotifications( CEconNotification *pNotification ) +{ + return ( CTFDuelRequestNotification::IsDuelRequestNotification( pNotification ) || + CTFDuelInfoNotification::IsDuelInfoNotification( pNotification ) ); +} + +static void DuelMiniGame_Reset() +{ + delete gDuelMiniGameLocalData.m_pEventListener; + gDuelMiniGameLocalData.m_pEventListener = NULL; + gDuelMiniGameLocalData.m_steamIDOpponent = CSteamID(); + gDuelMiniGameLocalData.m_unMyScore = 0; + gDuelMiniGameLocalData.m_unOpponentScore = 0; + gDuelMiniGameLocalData.m_iRequiredPlayerClass = TF_CLASS_UNDEFINED; +} + +bool DuelMiniGame_IsDuelingLocalPlayer( C_TFPlayer *pPlayer ) +{ + CSteamID steamID; + if ( pPlayer->GetSteamID( &steamID ) ) + { + return ( steamID == gDuelMiniGameLocalData.m_steamIDOpponent ); + } + return false; +} + +bool DuelMiniGame_GetStats( C_TFPlayer **ppPlayer, uint32 &unMyScore, uint32 &unOpponentScore ) +{ + *ppPlayer = NULL; + int iLocalPlayerIndex = GetLocalPlayerIndex(); + for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) + { + // find all players who are on the local player's team + if( ( iPlayerIndex != iLocalPlayerIndex ) && ( g_PR->IsConnected( iPlayerIndex ) ) ) + { + CSteamID steamID; + C_TFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( iPlayerIndex ) ); + if ( pPlayer && pPlayer->GetSteamID( &steamID ) && steamID == gDuelMiniGameLocalData.m_steamIDOpponent ) + { + *ppPlayer = pPlayer; + break; + } + } + } + static bool sbTesting = false; + if ( sbTesting ) + { + *ppPlayer = ToTFPlayer( C_BasePlayer::GetLocalPlayer() ); + } + if ( *ppPlayer != NULL ) + { + CSteamID steamID = steamapicontext->SteamUser()->GetSteamID(); + unMyScore = gDuelMiniGameLocalData.m_unMyScore; + unOpponentScore = gDuelMiniGameLocalData.m_unOpponentScore; + return true; + } + return false; +} + +/** + * Select player for duel dialog. + */ +class CSelectPlayerForDuelDialog : public CSelectPlayerDialog, public CGameEventListener +{ + DECLARE_CLASS_SIMPLE( CSelectPlayerForDuelDialog, CSelectPlayerDialog ); +public: + CSelectPlayerForDuelDialog( uint64 iItemID ); + virtual ~CSelectPlayerForDuelDialog(); + virtual void OnSelectPlayer( const CSteamID &steamID ); + virtual void ApplySchemeSettings( vgui::IScheme *pScheme ); + virtual void FireGameEvent( IGameEvent *event ); + + MESSAGE_FUNC_PARAMS( OnClassIconSelected, "ClassIconSelected", data ); + MESSAGE_FUNC_PARAMS( OnShowClassIconMouseover, "ShowClassIconMouseover", data ); + MESSAGE_FUNC( OnHideClassIconMouseover, "HideClassIconMouseover" ); + +protected: + virtual const char *GetResFile() { return "resource/ui/SelectPlayerDialog_Duel.res"; } + void SetSelectedClass( int iClass ); + void SetupClassImage( const char *pImageControlName, int iClass ); + + uint64 m_iItemID; + uint8 m_iPlayerClass; + vgui::Label *m_pClassIconMouseoverLabel; +}; + +static vgui::DHANDLE< CSelectPlayerForDuelDialog > g_pSelectPlayerForDuelingDialog; + +CSelectPlayerForDuelDialog::CSelectPlayerForDuelDialog( uint64 iItemID ) + : CSelectPlayerDialog( NULL ) + , m_iItemID( iItemID ) + , m_iPlayerClass( TF_CLASS_UNDEFINED ) +{ + g_pSelectPlayerForDuelingDialog = this; + m_bAllowSameTeam = false; + m_bAllowOutsideServer = false; + m_pClassIconMouseoverLabel = new vgui::Label( this, "ClassUsageMouseoverLabel", "" ); + ListenForGameEvent( "teamplay_round_win" ); + ListenForGameEvent( "teamplay_round_stalemate" ); +} + +CSelectPlayerForDuelDialog::~CSelectPlayerForDuelDialog() +{ + g_pSelectPlayerForDuelingDialog = NULL; +} + +void CSelectPlayerForDuelDialog::OnSelectPlayer( const CSteamID &steamID ) +{ + GCSDK::CProtoBufMsg<CMsgUseItem> msg( k_EMsgGCUseItemRequest ); + msg.Body().set_item_id( m_iItemID ); + msg.Body().set_target_steam_id( steamID.ConvertToUint64() ); + msg.Body().set_duel__class_lock( m_iPlayerClass ); + GCClientSystem()->BSendMessage( msg ); +} + +void CSelectPlayerForDuelDialog::ApplySchemeSettings( vgui::IScheme *pScheme ) +{ + CSelectPlayerDialog::ApplySchemeSettings( pScheme ); + SetDialogVariable( "title", g_pVGuiLocalize->Find( "TF_DuelDialog_Title" ) ); + + SetupClassImage( "ClassUsageImage_Any", TF_CLASS_UNDEFINED ); + + C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); + SetupClassImage( "ClassUsageImage_Locked", pLocalPlayer->m_Shared.GetDesiredPlayerClassIndex() ); + + SetSelectedClass( TF_CLASS_UNDEFINED ); +} + +void CSelectPlayerForDuelDialog::SetupClassImage( const char *pImageControlName, int iClass ) +{ + CStorePreviewClassIcon *pClassImage = dynamic_cast<CStorePreviewClassIcon*>( FindChildByName( pImageControlName ) ); + if ( pClassImage ) + { + pClassImage->SetClass( iClass ); + } +} + +void CSelectPlayerForDuelDialog::FireGameEvent( IGameEvent *event ) +{ + const char *pEventName = event->GetName(); + + C_TFPlayer *pLocalPlayer = C_TFPlayer::GetLocalTFPlayer(); + if ( pLocalPlayer == NULL ) + { + OnCommand( "cancel" ); + return; + } + + if ( Q_strcmp( "teamplay_round_win", pEventName ) == 0 || Q_strcmp( "teamplay_round_stalemate", pEventName ) == 0 ) + { + OnCommand( "cancel" ); + return; + } +} + +void CSelectPlayerForDuelDialog::OnClassIconSelected( KeyValues *data ) +{ + int iClass = data->GetInt( "class", 0 ); + SetSelectedClass( iClass ); +} + +void CSelectPlayerForDuelDialog::OnHideClassIconMouseover( void ) +{ + if ( m_pClassIconMouseoverLabel ) + { + m_pClassIconMouseoverLabel->SetVisible( false ); + } +} + +void CSelectPlayerForDuelDialog::OnShowClassIconMouseover( KeyValues *data ) +{ + if ( m_pClassIconMouseoverLabel ) + { + // Set the text to the correct string + int iClass = data->GetInt( "class", 0 ); + if ( iClass >= TF_FIRST_NORMAL_CLASS && iClass < TF_LAST_NORMAL_CLASS ) + { + wchar_t wzLocalized[256]; + const char *pszLocString = "#TF_SelectPlayer_Duel_PlayerClass"; + g_pVGuiLocalize->ConstructString_safe( wzLocalized, g_pVGuiLocalize->Find( pszLocString ), 1, g_pVGuiLocalize->Find( g_aPlayerClassNames[iClass] ) ); + m_pClassIconMouseoverLabel->SetText( wzLocalized ); + } + else + { + const char *pszLocString = "#TF_SelectPlayer_Duel_AnyClass"; + m_pClassIconMouseoverLabel->SetText( pszLocString ); + } + + m_pClassIconMouseoverLabel->SetVisible( true ); + } +} + +void CSelectPlayerForDuelDialog::SetSelectedClass( int iClass ) +{ + m_iPlayerClass = iClass; + + const char* pClassName = "#TF_SelectPlayer_DuelClass_None"; + + switch ( m_iPlayerClass ) + { + case TF_CLASS_SCOUT: pClassName = "#TF_Class_Name_Scout"; break; + case TF_CLASS_SNIPER: pClassName = "#TF_Class_Name_Sniper"; break; + case TF_CLASS_SOLDIER: pClassName = "#TF_Class_Name_Soldier"; break; + case TF_CLASS_DEMOMAN: pClassName = "#TF_Class_Name_Demoman"; break; + case TF_CLASS_MEDIC: pClassName = "#TF_Class_Name_Medic"; break; + case TF_CLASS_HEAVYWEAPONS: pClassName = "#TF_Class_Name_HWGuy"; break; + case TF_CLASS_PYRO: pClassName = "#TF_Class_Name_Pyro"; break; + case TF_CLASS_SPY: pClassName = "#TF_Class_Name_Spy"; break; + case TF_CLASS_ENGINEER: pClassName = "#TF_Class_Name_Engineer"; break; + } + + wchar_t wszText[1024]=L""; + g_pVGuiLocalize->ConstructString_safe( wszText, + g_pVGuiLocalize->Find( "#TF_SelectPlayer_DuelClass" ), + 1, + g_pVGuiLocalize->Find( pClassName ) ); + + SetDialogVariable( "player_class", wszText ); +} + +static void ShowSelectDuelTargetDialog( uint64 iItemID ) +{ + CSelectPlayerForDuelDialog *pDialog = vgui::SETUP_PANEL( new CSelectPlayerForDuelDialog( iItemID ) ); + pDialog->InvalidateLayout( false, true ); + + pDialog->Reset(); + pDialog->SetVisible( true ); + pDialog->MakePopup(); + pDialog->MoveToFront(); + pDialog->SetKeyBoardInputEnabled(true); + pDialog->SetMouseInputEnabled(true); + TFModalStack()->PushModal( pDialog ); +} + +//----------------------------------------------------------------------------- +void CL_Consumables_LevelShutdown() +{ + DuelMiniGame_Reset(); + if ( g_pSelectPlayerForDuelingDialog ) + { + g_pSelectPlayerForDuelingDialog->OnCommand( "cancel" ); + } + NotificationQueue_Remove( &CTFDuelRequestNotification::IsDuelRequestNotification ); +} + +// +// Players see this whenever a person on their server uses a name tool +// +class CTFNameItemNotification : public GCSDK::CGCClientJob +{ +public: + CTFNameItemNotification( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} + virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) + { + GCSDK::CProtoBufMsg<CMsgGCNameItemNotification> msg( pNetPacket ); + + if ( steamapicontext == NULL || steamapicontext->SteamUser() == NULL ) + return true; + + if ( !engine->IsInGame() ) + return true; + + CBaseHudChat *pHUDChat = (CBaseHudChat *)GET_HUDELEMENT( CHudChat ); + if ( pHUDChat ) + { + // Player + wchar_t wszPlayerName[MAX_PLAYER_NAME_LENGTH]; + char szPlayerName[ MAX_PLAYER_NAME_LENGTH ]; + + GetPlayerNameBySteamID( msg.Body().player_steamid(), szPlayerName, sizeof( szPlayerName ) ); + g_pVGuiLocalize->ConvertANSIToUnicode( szPlayerName, wszPlayerName, sizeof( wszPlayerName ) ); + + // Item + item_definition_index_t nDefIndex = msg.Body().item_def_index(); + const GameItemDefinition_t *pItemDefinition = dynamic_cast<GameItemDefinition_t *>( GetItemSchema()->GetItemDefinition( nDefIndex ) ); + if ( !pItemDefinition ) + return true; + entityquality_t iItemQuality = pItemDefinition->GetQuality(); + + // Name + wchar_t wszCustomName[MAX_ITEM_CUSTOM_NAME_LENGTH]; + g_pVGuiLocalize->ConvertANSIToUnicode( msg.Body().item_name_custom().c_str(), wszCustomName, sizeof( wszCustomName ) ); + + wchar_t wszItemRenamed[256]; + _snwprintf( wszItemRenamed, ARRAYSIZE( wszItemRenamed ), L"%ls", g_pVGuiLocalize->Find( "#Item_Named" ) ); + + const char *pszQualityColorString = EconQuality_GetColorString( (EEconItemQuality)iItemQuality ); + if ( pszQualityColorString ) + { + pHUDChat->SetCustomColor( pszQualityColorString ); + } + + wchar_t wszLocalizedString[256]; + g_pVGuiLocalize->ConstructString_safe( wszLocalizedString, wszItemRenamed, 3, wszPlayerName, CEconItemLocalizedFullNameGenerator( GLocalizationProvider(), pItemDefinition, iItemQuality ).GetFullName(), wszCustomName ); + + char szLocalized[256]; + g_pVGuiLocalize->ConvertUnicodeToANSI( wszLocalizedString, szLocalized, sizeof( szLocalized ) ); + + pHUDChat->ChatPrintf( 0, CHAT_FILTER_SERVERMSG, "%s", szLocalized ); + } + + return true; + } +}; +GC_REG_JOB( GCSDK::CGCClient, CTFNameItemNotification, "CTFNameItemNotification", k_EMsgGCNameItemNotification, GCSDK::k_EServerTypeGCClient ); + +// +// A wrapper for a generic message sent down from the GC to clients. The clients do localization +// and layout. +// +class CClientDisplayNotification : public CEconNotification +{ +public: + CClientDisplayNotification( GCSDK::IMsgNetPacket *pNetPacket ) + : m_msg( pNetPacket ) + { + SetText( m_msg.Body().notification_body_localization_key().c_str() ); + SetLifetime( 23.0f ); + + if ( m_msg.Body().body_substring_keys_size() == m_msg.Body().body_substring_values_size() ) + { + for ( int i = 0; i < m_msg.Body().body_substring_keys_size(); i++ ) + { + const char *pszSubstringKey = m_msg.Body().body_substring_keys( i ).c_str(); + const char *pszSubstringValue = m_msg.Body().body_substring_values( i ).c_str(); + + if ( pszSubstringValue[0] == '#' ) + { + AddStringToken( pszSubstringKey, GLocalizationProvider()->Find( pszSubstringValue ) ); + } + else + { + CUtlConstWideString wsSubstringValue; + GLocalizationProvider()->ConvertUTF8ToLocchar( pszSubstringValue, &wsSubstringValue ) ; + AddStringToken( pszSubstringKey, wsSubstringValue.Get() ); + } + } + } + } + +private: + // We make a local copy of the full message because dynamic-length strings are sent down from the + // GC and we need to make sure they stay in memory until the user looks at the notification. + GCSDK::CProtoBufMsg<CMsgGCClientDisplayNotification> m_msg; +}; + +class CTFClientDisplayNotification : public GCSDK::CGCClientJob +{ +public: + CTFClientDisplayNotification( GCSDK::CGCClient *pClient ) : GCSDK::CGCClientJob( pClient ) {} + virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket ) + { + GCSDK::CProtoBufMsg<CMsgGCClientDisplayNotification> msg( pNetPacket ); + + if ( steamapicontext == NULL || steamapicontext->SteamUser() == NULL ) + return true; + + NotificationQueue_Add( new CClientDisplayNotification( pNetPacket ) ); + + return true; + } +}; +GC_REG_JOB( GCSDK::CGCClient, CTFClientDisplayNotification, "CTFClientDisplayNotification", k_EMsgGCClientDisplayNotification, GCSDK::k_EServerTypeGCClient ); |