Team Fortress 2 Source Code as on 22/4/2020
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

566 lines
17 KiB

  1. //
  2. // Purpose:
  3. //
  4. // $NoKeywords: $
  5. //=============================================================================//
  6. #include "cbase.h"
  7. #include "shared_object_tracker.h"
  8. #include "gcsdk/gcclient.h"
  9. #include "gc_clientsystem.h"
  10. #ifdef CLIENT_DLL
  11. #include "econ_notifications.h"
  12. #include "clientmode_tf.h"
  13. #endif
  14. // memdbgon must be the last include file in a .cpp file!!!
  15. #include "tier0/memdbgon.h"
  16. short g_nQuestSpewFlags = 0;
  17. void SOTrackerSpew( const char* pszBuff, int nType )
  18. {
  19. if ( ( g_nQuestSpewFlags & nType ) == 0 )
  20. return;
  21. Color questDebugColor =
  22. #ifdef GAME_DLL
  23. Color( 255, 100, 0, 255 );
  24. ConColorMsg( questDebugColor, "[SVTrackers]: %s", pszBuff );
  25. #else
  26. Color( 255, 200, 0, 255 );
  27. ConColorMsg( questDebugColor, "[CLTrackers]: %s", pszBuff );
  28. #endif
  29. }
  30. void SOTrackerSpewTypeToggle( const CCommand &args )
  31. {
  32. if ( args.ArgC() != 2 )
  33. {
  34. Warning( "Incorrect parameters. Format: command_toggle_SO_TRACKER_SPEW_type <type>\n" );
  35. return;
  36. }
  37. CUtlString strType( args[1] );
  38. strType.ToLower();
  39. int nBitMask = 0;
  40. if ( FStrEq( strType, "objectives" ) )
  41. {
  42. nBitMask = SO_TRACKER_SPEW_OBJECTIVES;
  43. }
  44. else if ( FStrEq( strType, "itemtrackers" ) )
  45. {
  46. nBitMask = SO_TRACKER_SPEW_ITEM_TRACKER_MANAGEMENT;
  47. }
  48. else if ( FStrEq( strType, "objectivetrackers" ) )
  49. {
  50. nBitMask = SO_TRACKER_SPEW_OBJECTIVE_TRACKER_MANAGEMENT;
  51. }
  52. else if ( FStrEq( strType, "commits" ) )
  53. {
  54. nBitMask = SO_TRACKER_SPEW_GC_COMMITS;
  55. }
  56. else if ( FStrEq( strType, "socache" ) )
  57. {
  58. nBitMask = SO_TRACKER_SPEW_SOCACHE_ACTIVITY;
  59. }
  60. else if ( FStrEq( strType, "all" ) )
  61. {
  62. nBitMask = 0xFFFFFFFF;
  63. }
  64. if ( nBitMask == 0 )
  65. {
  66. Warning( "Invalid type. Valid types are: objectives, itemtrackers, objectivetrackers, commits, or all for everything\n" );
  67. return;
  68. }
  69. g_nQuestSpewFlags ^= nBitMask;
  70. DevMsg( "%s %s\n", strType.Get(), g_nQuestSpewFlags & nBitMask ? "ENABLED" : "DISABLED" );
  71. }
  72. ConCommand tf_so_tracker_spew_type_toggle( "tf_so_tracker_spew_type_toggle", SOTrackerSpewTypeToggle, NULL
  73. #ifdef CLIENT_DLL
  74. , FCVAR_CHEAT
  75. #endif
  76. );
  77. CBaseSOTracker::CBaseSOTracker( const CSharedObject* pSObject, CSteamID steamIDOwner, CSOTrackerManager* pManager )
  78. : m_pSObject( pSObject )
  79. , m_steamIDOwner( steamIDOwner )
  80. , m_pManager( pManager )
  81. {
  82. Assert( m_pSObject );
  83. Assert( m_pManager );
  84. }
  85. CBaseSOTracker::~CBaseSOTracker()
  86. {}
  87. void CBaseSOTracker::Spew() const
  88. {
  89. DevMsg( "Tracker for object type %d\n", m_pSObject->GetTypeID() );
  90. m_pSObject->Dump();
  91. }
  92. CSOTrackerManager::CSOTrackerManager()
  93. : m_mapItemTrackers( DefLessFunc( SOTrackerMap_t::KeyType_t ) )
  94. , m_mapUnacknowledgedCommits( DefLessFunc( CommitsMap_t::KeyType_t ) )
  95. #ifdef GAME_DLL
  96. , CAutoGameSystemPerFrame( "CSOTrackerManager" )
  97. #endif
  98. {}
  99. CSOTrackerManager::~CSOTrackerManager()
  100. {
  101. SO_TRACKER_SPEW( "Destroying CQuestObjectiveManager\n", SO_TRACKER_SPEW_ITEM_TRACKER_MANAGEMENT );
  102. Shutdown();
  103. }
  104. void CSOTrackerManager::Initialize()
  105. {
  106. ListenForGameEvent( "schema_updated" );
  107. #ifdef GAME_DLL
  108. ListenForGameEvent( "player_spawn" );
  109. ListenForGameEvent( "player_initial_spawn" );
  110. ListenForGameEvent( "server_spawn" );
  111. ListenForGameEvent( "server_shutdown" );
  112. ListenForGameEvent( "player_disconnect" );
  113. #endif
  114. }
  115. void CSOTrackerManager::Shutdown()
  116. {
  117. CommitAllChanges();
  118. m_mapItemTrackers.PurgeAndDeleteElements();
  119. }
  120. void CSOTrackerManager::FireGameEvent( IGameEvent *pEvent )
  121. {
  122. const char* pszName = pEvent->GetName();
  123. if ( FStrEq( pszName, "schema_updated" ) )
  124. {
  125. // Recreate all existing trackers
  126. m_mapItemTrackers.PurgeAndDeleteElements();
  127. CUtlVector< CSteamID > vecIDsToUpdate;
  128. #ifdef GAME_DLL
  129. // On the server, we need need new trackers for everyone
  130. for ( int i = 1; i<= gpGlobals->maxClients; i++)
  131. {
  132. CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );
  133. if ( pPlayer )
  134. {
  135. CSteamID& steamID = vecIDsToUpdate[ vecIDsToUpdate.AddToTail() ];
  136. pPlayer->GetSteamID( &steamID );
  137. }
  138. }
  139. #else
  140. // On the client we just need new trackers for us
  141. vecIDsToUpdate.AddToTail( steamapicontext->SteamUser()->GetSteamID() );
  142. #endif
  143. FOR_EACH_VEC( vecIDsToUpdate, i )
  144. {
  145. EnsureTrackersForPlayer( vecIDsToUpdate[ i ] );
  146. }
  147. }
  148. else if ( FStrEq( pszName, "server_spawn" ) )
  149. {
  150. CommitAllChanges();
  151. }
  152. else if ( FStrEq( pszName, "server_shutdown" ) )
  153. {
  154. Shutdown();
  155. }
  156. #ifdef GAME_DLL
  157. else if ( FStrEq( pszName, "player_disconnect" ) )
  158. {
  159. CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByUserId( pEvent->GetInt("userid") ) );
  160. if ( pPlayer )
  161. {
  162. CSteamID steamID;
  163. pPlayer->GetSteamID( &steamID );
  164. SO_TRACKER_SPEW( CFmtStr( "Unsubscribing from SOCache for user %s\n", steamID.Render() ), SO_TRACKER_SPEW_SOCACHE_ACTIVITY );
  165. GCClientSystem()->GetGCClient()->RemoveSOCacheListener( steamID, this );
  166. }
  167. }
  168. else if ( FStrEq( pszName, "player_spawn" ) )
  169. {
  170. const int nUserID = pEvent->GetInt( "userid" );
  171. CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByUserId( nUserID ) );
  172. EnsureTrackersForPlayer( pPlayer );
  173. }
  174. else if ( FStrEq( pszName, "player_initial_spawn" ) )
  175. {
  176. CTFPlayer *pNewPlayer = ToTFPlayer( UTIL_PlayerByIndex( pEvent->GetInt( "index" ) ) );
  177. Assert( pNewPlayer );
  178. // We want to listen for SO caches
  179. if ( pNewPlayer && !pNewPlayer->IsBot() )
  180. {
  181. CSteamID steamID;
  182. pNewPlayer->GetSteamID( &steamID );
  183. if( steamID.IsValid() )
  184. {
  185. SO_TRACKER_SPEW( CFmtStr( "Subscribing to SOCache for user %s\n", steamID.Render() ), SO_TRACKER_SPEW_SOCACHE_ACTIVITY );
  186. GCClientSystem()->GetGCClient()->AddSOCacheListener( steamID, this );
  187. EnsureTrackersForPlayer( steamID );
  188. }
  189. }
  190. }
  191. #endif
  192. }
  193. void CSOTrackerManager::SOCreated( const CSteamID & steamIDOwner, const CSharedObject *pObject, ESOCacheEvent eEvent )
  194. {
  195. HandleSOEvent( steamIDOwner, pObject, TRACKER_CREATE_OR_UPDATE );
  196. }
  197. void CSOTrackerManager::SOUpdated( const CSteamID & steamIDOwner, const CSharedObject *pObject, ESOCacheEvent eEvent )
  198. {
  199. HandleSOEvent( steamIDOwner, pObject, TRACKER_CREATE_OR_UPDATE );
  200. }
  201. void CSOTrackerManager::SODestroyed( const CSteamID & steamIDOwner, const CSharedObject *pObject, ESOCacheEvent eEvent)
  202. {
  203. HandleSOEvent( steamIDOwner, pObject, TRACKER_REMOVE );
  204. }
  205. void CSOTrackerManager::SOCacheSubscribed( const CSteamID & steamIDOwner, ESOCacheEvent eEvent )
  206. {
  207. SO_TRACKER_SPEW( CFmtStr( "SOCacheSubscribed recieved for user %s\n", steamIDOwner.Render() ), SO_TRACKER_SPEW_SOCACHE_ACTIVITY );
  208. // Clear out trackers that are all now invalid
  209. RemoveTrackersForSteamID( steamIDOwner );
  210. EnsureTrackersForPlayer( steamIDOwner );
  211. }
  212. void CSOTrackerManager::SOCacheUnsubscribed( const CSteamID & steamIDOwner, ESOCacheEvent eEvent )
  213. {
  214. SO_TRACKER_SPEW( CFmtStr( "SOCacheUnsubscribed recieved for user %s\n", steamIDOwner.Render() ), SO_TRACKER_SPEW_SOCACHE_ACTIVITY );
  215. RemoveTrackersForSteamID( steamIDOwner );
  216. }
  217. void CSOTrackerManager::HandleSOEvent( const CSteamID & steamIDOwner, const CSharedObject *pObject, ETrackerHandling_t eHandling )
  218. {
  219. if ( !ShouldTrackObject( steamIDOwner, pObject ) )
  220. return;
  221. UpdateTrackerForItem( pObject, eHandling, steamIDOwner );
  222. }
  223. CBaseSOTracker* CSOTrackerManager::GetTracker( SOTrackerMap_t::KeyType_t nKey ) const
  224. {
  225. auto idx = m_mapItemTrackers.Find( nKey );
  226. if ( idx != m_mapItemTrackers.InvalidIndex() )
  227. {
  228. return m_mapItemTrackers[ idx ];
  229. }
  230. return NULL;
  231. }
  232. CommitRecord_t* CSOTrackerManager::GetCommitRecord( CommitsMap_t::KeyType_t nKey )
  233. {
  234. auto idx = m_mapUnacknowledgedCommits.Find( nKey );
  235. if ( idx != m_mapUnacknowledgedCommits.InvalidIndex() )
  236. {
  237. return m_mapUnacknowledgedCommits[ idx ];
  238. }
  239. return NULL;
  240. }
  241. void CSOTrackerManager::UpdateTrackerForItem( const CSharedObject* pItem, ETrackerHandling_t eHandling, CSteamID steamIDOwner )
  242. {
  243. // Do we want to make sure we have a tracker, or that we dont have a tracker
  244. const bool bWantsTracker = eHandling != TRACKER_REMOVE;
  245. auto idx = m_mapItemTrackers.Find( GetKeyForObjectTracker( pItem, steamIDOwner ) );
  246. // Wants a tracker and doesnt have one?
  247. if ( bWantsTracker && idx == m_mapItemTrackers.InvalidIndex() )
  248. {
  249. CreateAndAddTracker( pItem, steamIDOwner );
  250. }
  251. else if ( !bWantsTracker && idx != m_mapItemTrackers.InvalidIndex() ) // Doesnt want a tracker and has one?
  252. {
  253. RemoveAndDeleteTrackerAtIndex( idx );
  254. }
  255. else if ( idx != m_mapItemTrackers.InvalidIndex() )
  256. {
  257. m_mapItemTrackers[ idx ]->OnUpdate();
  258. }
  259. }
  260. void CSOTrackerManager::EnsureTrackersForPlayer( const CSteamID& steamIDPlayer )
  261. {
  262. GCSDK::CGCClientSharedObjectCache *pSOCache = GCClientSystem()->GetSOCache( steamIDPlayer );
  263. if ( !pSOCache )
  264. return;
  265. CGCClientSharedObjectTypeCache *pSOTypeCache = pSOCache->FindTypeCache( GetType() );
  266. if ( !pSOTypeCache )
  267. {
  268. SO_TRACKER_SPEW( CFmtStr( "No SOCache for %s in %s!\n", steamIDPlayer.Render(), __FUNCTION__ ), SO_TRACKER_SPEW_ITEM_TRACKER_MANAGEMENT );
  269. return;
  270. }
  271. // Go through existing trackers and remove orphaned ones
  272. FOR_EACH_MAP_FAST( m_mapItemTrackers, i )
  273. {
  274. // If we didn't find the object in our cache, remove the tracker
  275. if ( m_mapItemTrackers[ i ]->GetOwnerSteamID() == steamIDPlayer &&
  276. pSOTypeCache->FindSharedObject( *m_mapItemTrackers[ i ]->GetSObject() ) == NULL )
  277. {
  278. RemoveAndDeleteTrackerAtIndex( i );
  279. i = -1;
  280. }
  281. }
  282. // Go through SOTypeCache and ensure we have trackers for every object
  283. for ( uint32 i=0; i < pSOTypeCache->GetCount(); ++i )
  284. {
  285. CSharedObject* pObject = pSOTypeCache->GetObject( i );
  286. if ( ShouldTrackObject( steamIDPlayer, pObject ) )
  287. {
  288. UpdateTrackerForItem( pObject, TRACKER_CREATE_OR_UPDATE, steamIDPlayer );
  289. }
  290. }
  291. }
  292. void CSOTrackerManager::EnsureTrackersForPlayer( CTFPlayer* pPlayer )
  293. {
  294. if ( pPlayer && !pPlayer->IsBot() )
  295. {
  296. CSteamID steamID;
  297. pPlayer->GetSteamID( &steamID );
  298. if( steamID.IsValid() )
  299. {
  300. EnsureTrackersForPlayer( steamID );
  301. }
  302. }
  303. }
  304. void CSOTrackerManager::CreateAndAddTracker( const CSharedObject* pItem, CSteamID steamIDOwner )
  305. {
  306. CBaseSOTracker* pItemTracker = AllocateNewTracker( pItem, steamIDOwner, this );
  307. auto nKey = GetKeyForObjectTracker( pItem, steamIDOwner );
  308. m_mapItemTrackers.Insert( nKey, pItemTracker );
  309. SO_TRACKER_SPEW( CFmtStr( "Created tracker for object: %s\n", GetDebugObjectDescription( pItem ).Get() ), SO_TRACKER_SPEW_ITEM_TRACKER_MANAGEMENT );
  310. }
  311. void CSOTrackerManager::RemoveAndDeleteTrackerAtIndex( SOTrackerMap_t::IndexType_t idx )
  312. {
  313. SO_TRACKER_SPEW( CFmtStr( "Deleted tracker for object: %s\n", GetDebugObjectDescription( m_mapItemTrackers[ idx ]->GetSObject() ).Get() ), SO_TRACKER_SPEW_ITEM_TRACKER_MANAGEMENT );
  314. delete m_mapItemTrackers[ idx ];
  315. m_mapItemTrackers.RemoveAt( idx );
  316. }
  317. void CSOTrackerManager::RemoveTrackersForSteamID( const CSteamID & steamIDOwner )
  318. {
  319. // We need to remove all trackers for the user
  320. FOR_EACH_MAP_FAST( m_mapItemTrackers, idx )
  321. {
  322. // Don't care about the itemIDs, just the steamID
  323. if ( m_mapItemTrackers[ idx ]->GetOwnerSteamID() == steamIDOwner )
  324. {
  325. m_mapItemTrackers[ idx ]->CommitChangesToDB();
  326. m_mapItemTrackers[ idx ]->OnRemove();
  327. delete m_mapItemTrackers[ idx ];
  328. m_mapItemTrackers.RemoveAt( idx );
  329. idx = -1; // Reset to be safe
  330. }
  331. }
  332. }
  333. void CSOTrackerManager::CommitAllChanges()
  334. {
  335. // Commit everything
  336. FOR_EACH_MAP_FAST( m_mapItemTrackers, idx )
  337. {
  338. m_mapItemTrackers[ idx ]->CommitChangesToDB();
  339. }
  340. }
  341. void CSOTrackerManager::Spew()
  342. {
  343. DevMsg( "--- Spewing all trackers for %s ---\n", GetName() );
  344. FOR_EACH_MAP( m_mapItemTrackers, i )
  345. {
  346. const CBaseSOTracker* pTracker = m_mapItemTrackers[ i ];
  347. CSteamID steamID( m_mapItemTrackers.Key( i ) );
  348. DevMsg( "\tTrackers for %s:\n", steamID.Render() );
  349. pTracker->Spew();
  350. DevMsg( "\t---\n" );
  351. }
  352. }
  353. #ifdef GAME_DLL
  354. void CSOTrackerManager::CommitRecord( CommitRecord_t* pRecord ) const
  355. {
  356. 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 );
  357. SendMessageForCommit( pRecord->m_pProtoMsg );
  358. pRecord->m_flLastCommitTime = Plat_FloatTime();
  359. }
  360. void CSOTrackerManager::FrameUpdatePreEntityThink()
  361. {
  362. // Rate limit to once a second
  363. double flNextCommitTime = m_flLastUnacknowledgeCommitTime + 1.f;
  364. double flNow = Plat_FloatTime();
  365. if ( flNow > flNextCommitTime )
  366. {
  367. m_flLastUnacknowledgeCommitTime = flNow;
  368. auto i = m_mapUnacknowledgedCommits.FirstInorder();
  369. while( i != m_mapUnacknowledgedCommits.InvalidIndex() )
  370. {
  371. auto currentIndex = i;
  372. i = m_mapUnacknowledgedCommits.NextInorder( i );
  373. // Give records 10 minutes to get themselves reported and acknowledged
  374. if ( flNow - m_mapUnacknowledgedCommits[ currentIndex ]->m_flReportedTime > 600.f )
  375. {
  376. 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 );
  377. m_mapUnacknowledgedCommits.RemoveAt( currentIndex );
  378. }
  379. else if ( m_mapUnacknowledgedCommits[ currentIndex ]->m_flLastCommitTime + 30.f < flNow )
  380. {
  381. // Only try committing for a given contract once every 30 seconds
  382. CommitRecord( m_mapUnacknowledgedCommits[ currentIndex ] );
  383. }
  384. }
  385. }
  386. }
  387. //-----------------------------------------------------------------------------
  388. // Purpose: Add a record of a commit to the GC. This is so we can listen for a
  389. // response from the GC (or lack thereof) and attempt to re-commit if needed
  390. //-----------------------------------------------------------------------------
  391. void CSOTrackerManager::AddCommitRecord( const ::google::protobuf::Message* pRecord, uint64 nKey, bool bRequireResponse )
  392. {
  393. // If we don't require a response, don't create a commit record that we have to track. Just commit right now
  394. if ( !bRequireResponse )
  395. {
  396. SendMessageForCommit( pRecord );
  397. return;
  398. }
  399. bool bShouldCommitNow = false;
  400. // Check if there's no record for this commit
  401. auto idx = m_mapUnacknowledgedCommits.Find( nKey );
  402. if ( idx == m_mapUnacknowledgedCommits.InvalidIndex() )
  403. {
  404. // Add it if nothing for this item
  405. ::google::protobuf::Message* pCopy = AllocateNewProtoMessage();
  406. pCopy->CopyFrom( *pRecord );
  407. idx = m_mapUnacknowledgedCommits.Insert( nKey, new CommitRecord_t( pCopy ) );
  408. bShouldCommitNow = true;
  409. SO_TRACKER_SPEW( CFmtStr( "Creating new commit record for SObject: %s\n", pRecord->DebugString().c_str() ), SO_TRACKER_SPEW_GC_COMMITS );
  410. }
  411. else
  412. {
  413. ::google::protobuf::Message* pExisting = m_mapUnacknowledgedCommits[ idx ]->m_pProtoMsg;
  414. // Check if this new record is more up to date than an existing commit record. If so, update the existing one
  415. if ( CompareRecords( pRecord, pExisting ) > 0 )
  416. {
  417. pExisting->CopyFrom( *pRecord );
  418. bShouldCommitNow = true;
  419. SO_TRACKER_SPEW( CFmtStr( "Updating existing commit record for SObject: %s\n", pRecord->DebugString().c_str() ), SO_TRACKER_SPEW_GC_COMMITS );
  420. }
  421. else
  422. {
  423. 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 );
  424. }
  425. }
  426. if ( bShouldCommitNow )
  427. {
  428. CommitRecord( m_mapUnacknowledgedCommits[ idx ] );
  429. }
  430. }
  431. //-----------------------------------------------------------------------------
  432. // Purpose: Handle the GC responding to an earlier commit. Remove any unacknowledged
  433. // commits records we have.
  434. //-----------------------------------------------------------------------------
  435. void CSOTrackerManager::AcknowledgeCommit( const ::google::protobuf::Message* pRecord, uint64 nKey )
  436. {
  437. OnCommitRecieved( pRecord );
  438. // Find the record
  439. auto idx = m_mapUnacknowledgedCommits.Find( nKey );
  440. if ( idx != m_mapUnacknowledgedCommits.InvalidIndex() )
  441. {
  442. ::google::protobuf::Message* pCommitRecord = m_mapUnacknowledgedCommits[ idx ]->m_pProtoMsg;
  443. // See if we have a matching record. If so, remove it
  444. if ( CompareRecords( pCommitRecord, pRecord ) == 0 )
  445. {
  446. SO_TRACKER_SPEW( CFmtStr( "Got matched response for with record: %s\n", pRecord->DebugString().c_str() ), SO_TRACKER_SPEW_GC_COMMITS );
  447. delete m_mapUnacknowledgedCommits[ idx ];
  448. m_mapUnacknowledgedCommits.RemoveAt( idx );
  449. }
  450. else
  451. {
  452. SO_TRACKER_SPEW( CFmtStr( "Ignoring stale response with record: %s\n", pRecord->DebugString().c_str() ), SO_TRACKER_SPEW_GC_COMMITS );
  453. }
  454. }
  455. }
  456. //-----------------------------------------------------------------------------
  457. // Purpose: Force a spew of all unacknowledged commits
  458. //-----------------------------------------------------------------------------
  459. void CSOTrackerManager::DBG_SpewPendingCommits()
  460. {
  461. SO_TRACKER_SPEW( CFmtStr( "Unacknowledged commits: %d\n", m_mapUnacknowledgedCommits.Count() ), SO_TRACKER_SPEW_GC_COMMITS );
  462. FOR_EACH_MAP( m_mapUnacknowledgedCommits, i )
  463. {
  464. SO_TRACKER_SPEW( CFmtStr( "%d: %s\n", i, m_mapUnacknowledgedCommits[ i ]->m_pProtoMsg->DebugString().c_str() ), SO_TRACKER_SPEW_GC_COMMITS );
  465. }
  466. }
  467. #if ( defined( DEBUG ) || defined( STAGING_ONLY ) ) && defined( GAME_DLL )
  468. CON_COMMAND( tf_quests_spew_unacknowledged_commits, "Spews info on all unacknowledged commits" )
  469. {
  470. // QuestObjectiveManager()->DBG_SpewPendingCommits();
  471. }
  472. #endif
  473. #endif