// // Purpose: // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "shared_object_tracker.h" #include "gcsdk/gcclient.h" #include "gc_clientsystem.h" #ifdef CLIENT_DLL #include "econ_notifications.h" #include "clientmode_tf.h" #endif // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" short g_nQuestSpewFlags = 0; void SOTrackerSpew( const char* pszBuff, int nType ) { if ( ( g_nQuestSpewFlags & nType ) == 0 ) return; Color questDebugColor = #ifdef GAME_DLL Color( 255, 100, 0, 255 ); ConColorMsg( questDebugColor, "[SVTrackers]: %s", pszBuff ); #else Color( 255, 200, 0, 255 ); ConColorMsg( questDebugColor, "[CLTrackers]: %s", pszBuff ); #endif } void SOTrackerSpewTypeToggle( const CCommand &args ) { if ( args.ArgC() != 2 ) { Warning( "Incorrect parameters. Format: command_toggle_SO_TRACKER_SPEW_type \n" ); return; } CUtlString strType( args[1] ); strType.ToLower(); int nBitMask = 0; if ( FStrEq( strType, "objectives" ) ) { nBitMask = SO_TRACKER_SPEW_OBJECTIVES; } else if ( FStrEq( strType, "itemtrackers" ) ) { nBitMask = SO_TRACKER_SPEW_ITEM_TRACKER_MANAGEMENT; } else if ( FStrEq( strType, "objectivetrackers" ) ) { nBitMask = SO_TRACKER_SPEW_OBJECTIVE_TRACKER_MANAGEMENT; } else if ( FStrEq( strType, "commits" ) ) { nBitMask = SO_TRACKER_SPEW_GC_COMMITS; } else if ( FStrEq( strType, "socache" ) ) { nBitMask = SO_TRACKER_SPEW_SOCACHE_ACTIVITY; } else if ( FStrEq( strType, "all" ) ) { nBitMask = 0xFFFFFFFF; } if ( nBitMask == 0 ) { Warning( "Invalid type. Valid types are: objectives, itemtrackers, objectivetrackers, commits, or all for everything\n" ); return; } g_nQuestSpewFlags ^= nBitMask; DevMsg( "%s %s\n", strType.Get(), g_nQuestSpewFlags & nBitMask ? "ENABLED" : "DISABLED" ); } ConCommand tf_so_tracker_spew_type_toggle( "tf_so_tracker_spew_type_toggle", SOTrackerSpewTypeToggle, NULL #ifdef CLIENT_DLL , FCVAR_CHEAT #endif ); CBaseSOTracker::CBaseSOTracker( const CSharedObject* pSObject, CSteamID steamIDOwner, CSOTrackerManager* pManager ) : m_pSObject( pSObject ) , m_steamIDOwner( steamIDOwner ) , m_pManager( pManager ) { Assert( m_pSObject ); Assert( m_pManager ); } CBaseSOTracker::~CBaseSOTracker() {} void CBaseSOTracker::Spew() const { DevMsg( "Tracker for object type %d\n", m_pSObject->GetTypeID() ); m_pSObject->Dump(); } CSOTrackerManager::CSOTrackerManager() : m_mapItemTrackers( DefLessFunc( SOTrackerMap_t::KeyType_t ) ) , m_mapUnacknowledgedCommits( DefLessFunc( CommitsMap_t::KeyType_t ) ) #ifdef GAME_DLL , CAutoGameSystemPerFrame( "CSOTrackerManager" ) #endif {} CSOTrackerManager::~CSOTrackerManager() { SO_TRACKER_SPEW( "Destroying CQuestObjectiveManager\n", SO_TRACKER_SPEW_ITEM_TRACKER_MANAGEMENT ); Shutdown(); } void CSOTrackerManager::Initialize() { ListenForGameEvent( "schema_updated" ); #ifdef GAME_DLL ListenForGameEvent( "player_spawn" ); ListenForGameEvent( "player_initial_spawn" ); ListenForGameEvent( "server_spawn" ); ListenForGameEvent( "server_shutdown" ); ListenForGameEvent( "player_disconnect" ); #endif } void CSOTrackerManager::Shutdown() { CommitAllChanges(); m_mapItemTrackers.PurgeAndDeleteElements(); } void CSOTrackerManager::FireGameEvent( IGameEvent *pEvent ) { const char* pszName = pEvent->GetName(); if ( FStrEq( pszName, "schema_updated" ) ) { // Recreate all existing trackers m_mapItemTrackers.PurgeAndDeleteElements(); CUtlVector< CSteamID > vecIDsToUpdate; #ifdef GAME_DLL // On the server, we need need new trackers for everyone for ( int i = 1; i<= gpGlobals->maxClients; i++) { CBasePlayer *pPlayer = UTIL_PlayerByIndex( i ); if ( pPlayer ) { CSteamID& steamID = vecIDsToUpdate[ vecIDsToUpdate.AddToTail() ]; pPlayer->GetSteamID( &steamID ); } } #else // On the client we just need new trackers for us vecIDsToUpdate.AddToTail( steamapicontext->SteamUser()->GetSteamID() ); #endif FOR_EACH_VEC( vecIDsToUpdate, i ) { EnsureTrackersForPlayer( vecIDsToUpdate[ i ] ); } } else if ( FStrEq( pszName, "server_spawn" ) ) { CommitAllChanges(); } else if ( FStrEq( pszName, "server_shutdown" ) ) { Shutdown(); } #ifdef GAME_DLL else if ( FStrEq( pszName, "player_disconnect" ) ) { CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByUserId( pEvent->GetInt("userid") ) ); if ( pPlayer ) { CSteamID steamID; pPlayer->GetSteamID( &steamID ); SO_TRACKER_SPEW( CFmtStr( "Unsubscribing from SOCache for user %s\n", steamID.Render() ), SO_TRACKER_SPEW_SOCACHE_ACTIVITY ); GCClientSystem()->GetGCClient()->RemoveSOCacheListener( steamID, this ); } } else if ( FStrEq( pszName, "player_spawn" ) ) { const int nUserID = pEvent->GetInt( "userid" ); CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByUserId( nUserID ) ); EnsureTrackersForPlayer( pPlayer ); } else if ( FStrEq( pszName, "player_initial_spawn" ) ) { CTFPlayer *pNewPlayer = ToTFPlayer( UTIL_PlayerByIndex( pEvent->GetInt( "index" ) ) ); Assert( pNewPlayer ); // We want to listen for SO caches if ( pNewPlayer && !pNewPlayer->IsBot() ) { CSteamID steamID; pNewPlayer->GetSteamID( &steamID ); if( steamID.IsValid() ) { SO_TRACKER_SPEW( CFmtStr( "Subscribing to SOCache for user %s\n", steamID.Render() ), SO_TRACKER_SPEW_SOCACHE_ACTIVITY ); GCClientSystem()->GetGCClient()->AddSOCacheListener( steamID, this ); EnsureTrackersForPlayer( steamID ); } } } #endif } void CSOTrackerManager::SOCreated( const CSteamID & steamIDOwner, const CSharedObject *pObject, ESOCacheEvent eEvent ) { HandleSOEvent( steamIDOwner, pObject, TRACKER_CREATE_OR_UPDATE ); } void CSOTrackerManager::SOUpdated( const CSteamID & steamIDOwner, const CSharedObject *pObject, ESOCacheEvent eEvent ) { HandleSOEvent( steamIDOwner, pObject, TRACKER_CREATE_OR_UPDATE ); } void CSOTrackerManager::SODestroyed( const CSteamID & steamIDOwner, const CSharedObject *pObject, ESOCacheEvent eEvent) { HandleSOEvent( steamIDOwner, pObject, TRACKER_REMOVE ); } void CSOTrackerManager::SOCacheSubscribed( const CSteamID & steamIDOwner, ESOCacheEvent eEvent ) { SO_TRACKER_SPEW( CFmtStr( "SOCacheSubscribed recieved for user %s\n", steamIDOwner.Render() ), SO_TRACKER_SPEW_SOCACHE_ACTIVITY ); // Clear out trackers that are all now invalid RemoveTrackersForSteamID( steamIDOwner ); EnsureTrackersForPlayer( steamIDOwner ); } void CSOTrackerManager::SOCacheUnsubscribed( const CSteamID & steamIDOwner, ESOCacheEvent eEvent ) { SO_TRACKER_SPEW( CFmtStr( "SOCacheUnsubscribed recieved for user %s\n", steamIDOwner.Render() ), SO_TRACKER_SPEW_SOCACHE_ACTIVITY ); RemoveTrackersForSteamID( steamIDOwner ); } void CSOTrackerManager::HandleSOEvent( const CSteamID & steamIDOwner, const CSharedObject *pObject, ETrackerHandling_t eHandling ) { if ( !ShouldTrackObject( steamIDOwner, pObject ) ) return; UpdateTrackerForItem( pObject, eHandling, steamIDOwner ); } CBaseSOTracker* CSOTrackerManager::GetTracker( SOTrackerMap_t::KeyType_t nKey ) const { auto idx = m_mapItemTrackers.Find( nKey ); if ( idx != m_mapItemTrackers.InvalidIndex() ) { return m_mapItemTrackers[ idx ]; } return NULL; } CommitRecord_t* CSOTrackerManager::GetCommitRecord( CommitsMap_t::KeyType_t nKey ) { auto idx = m_mapUnacknowledgedCommits.Find( nKey ); if ( idx != m_mapUnacknowledgedCommits.InvalidIndex() ) { return m_mapUnacknowledgedCommits[ idx ]; } return NULL; } void CSOTrackerManager::UpdateTrackerForItem( const CSharedObject* pItem, ETrackerHandling_t eHandling, CSteamID steamIDOwner ) { // Do we want to make sure we have a tracker, or that we dont have a tracker const bool bWantsTracker = eHandling != TRACKER_REMOVE; auto idx = m_mapItemTrackers.Find( GetKeyForObjectTracker( pItem, steamIDOwner ) ); // Wants a tracker and doesnt have one? if ( bWantsTracker && idx == m_mapItemTrackers.InvalidIndex() ) { CreateAndAddTracker( pItem, steamIDOwner ); } else if ( !bWantsTracker && idx != m_mapItemTrackers.InvalidIndex() ) // Doesnt want a tracker and has one? { RemoveAndDeleteTrackerAtIndex( idx ); } else if ( idx != m_mapItemTrackers.InvalidIndex() ) { m_mapItemTrackers[ idx ]->OnUpdate(); } } void CSOTrackerManager::EnsureTrackersForPlayer( const CSteamID& steamIDPlayer ) { GCSDK::CGCClientSharedObjectCache *pSOCache = GCClientSystem()->GetSOCache( steamIDPlayer ); if ( !pSOCache ) return; CGCClientSharedObjectTypeCache *pSOTypeCache = pSOCache->FindTypeCache( GetType() ); if ( !pSOTypeCache ) { SO_TRACKER_SPEW( CFmtStr( "No SOCache for %s in %s!\n", steamIDPlayer.Render(), __FUNCTION__ ), SO_TRACKER_SPEW_ITEM_TRACKER_MANAGEMENT ); return; } // Go through existing trackers and remove orphaned ones FOR_EACH_MAP_FAST( m_mapItemTrackers, i ) { // If we didn't find the object in our cache, remove the tracker if ( m_mapItemTrackers[ i ]->GetOwnerSteamID() == steamIDPlayer && pSOTypeCache->FindSharedObject( *m_mapItemTrackers[ i ]->GetSObject() ) == NULL ) { RemoveAndDeleteTrackerAtIndex( i ); i = -1; } } // Go through SOTypeCache and ensure we have trackers for every object for ( uint32 i=0; i < pSOTypeCache->GetCount(); ++i ) { CSharedObject* pObject = pSOTypeCache->GetObject( i ); if ( ShouldTrackObject( steamIDPlayer, pObject ) ) { UpdateTrackerForItem( pObject, TRACKER_CREATE_OR_UPDATE, steamIDPlayer ); } } } void CSOTrackerManager::EnsureTrackersForPlayer( CTFPlayer* pPlayer ) { if ( pPlayer && !pPlayer->IsBot() ) { CSteamID steamID; pPlayer->GetSteamID( &steamID ); if( steamID.IsValid() ) { EnsureTrackersForPlayer( steamID ); } } } void CSOTrackerManager::CreateAndAddTracker( const CSharedObject* pItem, CSteamID steamIDOwner ) { CBaseSOTracker* pItemTracker = AllocateNewTracker( pItem, steamIDOwner, this ); auto nKey = GetKeyForObjectTracker( pItem, steamIDOwner ); m_mapItemTrackers.Insert( nKey, pItemTracker ); SO_TRACKER_SPEW( CFmtStr( "Created tracker for object: %s\n", GetDebugObjectDescription( pItem ).Get() ), SO_TRACKER_SPEW_ITEM_TRACKER_MANAGEMENT ); } void CSOTrackerManager::RemoveAndDeleteTrackerAtIndex( SOTrackerMap_t::IndexType_t idx ) { SO_TRACKER_SPEW( CFmtStr( "Deleted tracker for object: %s\n", GetDebugObjectDescription( m_mapItemTrackers[ idx ]->GetSObject() ).Get() ), SO_TRACKER_SPEW_ITEM_TRACKER_MANAGEMENT ); delete m_mapItemTrackers[ idx ]; m_mapItemTrackers.RemoveAt( idx ); } void CSOTrackerManager::RemoveTrackersForSteamID( const CSteamID & steamIDOwner ) { // We need to remove all trackers for the user FOR_EACH_MAP_FAST( m_mapItemTrackers, idx ) { // Don't care about the itemIDs, just the steamID if ( m_mapItemTrackers[ idx ]->GetOwnerSteamID() == steamIDOwner ) { m_mapItemTrackers[ idx ]->CommitChangesToDB(); m_mapItemTrackers[ idx ]->OnRemove(); delete m_mapItemTrackers[ idx ]; m_mapItemTrackers.RemoveAt( idx ); idx = -1; // Reset to be safe } } } void CSOTrackerManager::CommitAllChanges() { // Commit everything FOR_EACH_MAP_FAST( m_mapItemTrackers, idx ) { m_mapItemTrackers[ idx ]->CommitChangesToDB(); } } void CSOTrackerManager::Spew() { DevMsg( "--- Spewing all trackers for %s ---\n", GetName() ); FOR_EACH_MAP( m_mapItemTrackers, i ) { const CBaseSOTracker* pTracker = m_mapItemTrackers[ i ]; CSteamID steamID( m_mapItemTrackers.Key( i ) ); DevMsg( "\tTrackers for %s:\n", steamID.Render() ); pTracker->Spew(); DevMsg( "\t---\n" ); } } #ifdef GAME_DLL void CSOTrackerManager::CommitRecord( CommitRecord_t* pRecord ) const { SO_TRACKER_SPEW( CFmtStr( "Sending %fs old record to GC for SObject. %s\n", Plat_FloatTime() - pRecord->m_flReportedTime, pRecord->m_pProtoMsg->DebugString().c_str() ), SO_TRACKER_SPEW_GC_COMMITS ); SendMessageForCommit( pRecord->m_pProtoMsg ); pRecord->m_flLastCommitTime = Plat_FloatTime(); } void CSOTrackerManager::FrameUpdatePreEntityThink() { // Rate limit to once a second double flNextCommitTime = m_flLastUnacknowledgeCommitTime + 1.f; double flNow = Plat_FloatTime(); if ( flNow > flNextCommitTime ) { m_flLastUnacknowledgeCommitTime = flNow; auto i = m_mapUnacknowledgedCommits.FirstInorder(); while( i != m_mapUnacknowledgedCommits.InvalidIndex() ) { auto currentIndex = i; i = m_mapUnacknowledgedCommits.NextInorder( i ); // Give records 10 minutes to get themselves reported and acknowledged if ( flNow - m_mapUnacknowledgedCommits[ currentIndex ]->m_flReportedTime > 600.f ) { SO_TRACKER_SPEW( CFmtStr( "Record is %fs old. Abandoning. %s\n", m_mapUnacknowledgedCommits[ currentIndex ]->m_flReportedTime, m_mapUnacknowledgedCommits[ currentIndex ]->m_pProtoMsg->DebugString().c_str() ), SO_TRACKER_SPEW_GC_COMMITS ); m_mapUnacknowledgedCommits.RemoveAt( currentIndex ); } else if ( m_mapUnacknowledgedCommits[ currentIndex ]->m_flLastCommitTime + 30.f < flNow ) { // Only try committing for a given contract once every 30 seconds CommitRecord( m_mapUnacknowledgedCommits[ currentIndex ] ); } } } } //----------------------------------------------------------------------------- // Purpose: Add a record of a commit to the GC. This is so we can listen for a // response from the GC (or lack thereof) and attempt to re-commit if needed //----------------------------------------------------------------------------- void CSOTrackerManager::AddCommitRecord( const ::google::protobuf::Message* pRecord, uint64 nKey, bool bRequireResponse ) { // If we don't require a response, don't create a commit record that we have to track. Just commit right now if ( !bRequireResponse ) { SendMessageForCommit( pRecord ); return; } bool bShouldCommitNow = false; // Check if there's no record for this commit auto idx = m_mapUnacknowledgedCommits.Find( nKey ); if ( idx == m_mapUnacknowledgedCommits.InvalidIndex() ) { // Add it if nothing for this item ::google::protobuf::Message* pCopy = AllocateNewProtoMessage(); pCopy->CopyFrom( *pRecord ); idx = m_mapUnacknowledgedCommits.Insert( nKey, new CommitRecord_t( pCopy ) ); bShouldCommitNow = true; SO_TRACKER_SPEW( CFmtStr( "Creating new commit record for SObject: %s\n", pRecord->DebugString().c_str() ), SO_TRACKER_SPEW_GC_COMMITS ); } else { ::google::protobuf::Message* pExisting = m_mapUnacknowledgedCommits[ idx ]->m_pProtoMsg; // Check if this new record is more up to date than an existing commit record. If so, update the existing one if ( CompareRecords( pRecord, pExisting ) > 0 ) { pExisting->CopyFrom( *pRecord ); bShouldCommitNow = true; SO_TRACKER_SPEW( CFmtStr( "Updating existing commit record for SObject: %s\n", pRecord->DebugString().c_str() ), SO_TRACKER_SPEW_GC_COMMITS ); } else { SO_TRACKER_SPEW( CFmtStr( "Existing commit record for SObject is more up to date: %s\n", pExisting->DebugString().c_str() ), SO_TRACKER_SPEW_GC_COMMITS ); } } if ( bShouldCommitNow ) { CommitRecord( m_mapUnacknowledgedCommits[ idx ] ); } } //----------------------------------------------------------------------------- // Purpose: Handle the GC responding to an earlier commit. Remove any unacknowledged // commits records we have. //----------------------------------------------------------------------------- void CSOTrackerManager::AcknowledgeCommit( const ::google::protobuf::Message* pRecord, uint64 nKey ) { OnCommitRecieved( pRecord ); // Find the record auto idx = m_mapUnacknowledgedCommits.Find( nKey ); if ( idx != m_mapUnacknowledgedCommits.InvalidIndex() ) { ::google::protobuf::Message* pCommitRecord = m_mapUnacknowledgedCommits[ idx ]->m_pProtoMsg; // See if we have a matching record. If so, remove it if ( CompareRecords( pCommitRecord, pRecord ) == 0 ) { SO_TRACKER_SPEW( CFmtStr( "Got matched response for with record: %s\n", pRecord->DebugString().c_str() ), SO_TRACKER_SPEW_GC_COMMITS ); delete m_mapUnacknowledgedCommits[ idx ]; m_mapUnacknowledgedCommits.RemoveAt( idx ); } else { SO_TRACKER_SPEW( CFmtStr( "Ignoring stale response with record: %s\n", pRecord->DebugString().c_str() ), SO_TRACKER_SPEW_GC_COMMITS ); } } } //----------------------------------------------------------------------------- // Purpose: Force a spew of all unacknowledged commits //----------------------------------------------------------------------------- void CSOTrackerManager::DBG_SpewPendingCommits() { SO_TRACKER_SPEW( CFmtStr( "Unacknowledged commits: %d\n", m_mapUnacknowledgedCommits.Count() ), SO_TRACKER_SPEW_GC_COMMITS ); FOR_EACH_MAP( m_mapUnacknowledgedCommits, i ) { SO_TRACKER_SPEW( CFmtStr( "%d: %s\n", i, m_mapUnacknowledgedCommits[ i ]->m_pProtoMsg->DebugString().c_str() ), SO_TRACKER_SPEW_GC_COMMITS ); } } #if ( defined( DEBUG ) || defined( STAGING_ONLY ) ) && defined( GAME_DLL ) CON_COMMAND( tf_quests_spew_unacknowledged_commits, "Spews info on all unacknowledged commits" ) { // QuestObjectiveManager()->DBG_SpewPendingCommits(); } #endif #endif