source-engine/gcsdk/gcbase.cpp

4433 lines
151 KiB
C++
Raw Permalink Normal View History

2020-04-23 00:56:21 +08:00
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
// $NoKeywords: $
//=============================================================================//
#include "stdafx.h"
#include "gcbase.h"
#include "tier1/interface.h"
#include "tier0/minidump.h"
#include "tier0/icommandline.h"
#include "gcjob.h"
#include "sqlaccess/schemaupdate.h"
#include "gcsystemmsgs.h"
#include "rtime.h"
#include "msgprotobuf.h"
#include "gcsdk_gcmessages.pb.h"
#include "gcsdk/gcparalleljobfarm.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
namespace GCSDK
{
//----------------------------------------------------------------------
// Emit groups
//----------------------------------------------------------------------
DECLARE_GC_EMIT_GROUP( g_EGHTTPRequest, http_request );
CGCBase *g_pGCBase = NULL;
// Thread pool size convar
static void OnConVarChangeJobMgrThreadPoolSize( IConVar *pConVar, const char *pOldString, float flOldValue );
GCConVar jobmgr_threadpool_size( "jobmgr_threadpool_size", "-1", 0,
"Maximum threads in the job manager thread pool. Values <= 0 mean number_logical_cpus - this.",
OnConVarChangeJobMgrThreadPoolSize );
static uint32 GetThreadPoolSizeFromConVar()
{
int nVal = jobmgr_threadpool_size.GetInt();
int nRet = ( nVal > 0 ) ? nVal : GetCPUInformation()->m_nLogicalProcessors + nVal;
return (uint32)Clamp( nRet, 1, INT_MAX );
}
static void OnConVarChangeJobMgrThreadPoolSize( IConVar *pConVar, const char *pOldString, float flOldValue )
{
if ( GGCBase()->GetIsShuttingDown() )
return;
GGCBase()->GetJobMgr().SetThreadPoolSize( GetThreadPoolSizeFromConVar() );
}
GCConVar cv_concurrent_start_playing_limit( "concurrent_start_playing_limit", "1000" );
GCConVar cv_logon_surge_start_playing_limit( "logon_surge_start_playing_limit", "2000" );
GCConVar cv_logon_surge_request_session_jobs( "logon_surge_request_session_jobs", "1000" );
GCConVar cv_webapi_throttle_job_threshold( "webapi_throttle_job_threshold", "2000", 0, "If the job count exceeds this threshold, reject low-priority webapi jobs" );
GCConVar enable_startplaying_gameserver_creation_spew( "enable_startplaying_gameserver_creation_spew", "0" );
// Enable the restore-version-from-memcache machinery. Disabled because it assumes reloading an SOCache is
// deterministic, which is no longer true for us, resulting in clients with stale versions believing themselves to be in
// sync.
//
// This probably needs a look -- ideally we'd delineate deterministic objects that can be assumed to remain in sync in
// GC reboots, and dynamic objects that cannot.
//
// Note that we already removed hacks for this in player groups and started using lazy-loaded objects in SOCaches that
// violate the assumptions this was making, so re-enabling it requires work. We probably really want to split type
// caches into deterministic-between-GC-reboots and not, and resend based on said flag.
GCConVar socache_persist_version_via_memcached( "socache_persist_version_via_memcached", "0" );
static GCConVar cv_assert_minidump_window( "assert_minidump_window", "28800", 0, "Size of the minidump window in seconds. Each unique assert will dump at most assert_max_minidumps_in_window times in this many seconds" );
static GCConVar cv_assert_max_minidumps_in_window( "assert_max_minidumps_in_window", "5", 0, "The amount of times each unique assert will write a dump in assert_minidump_window seconds" );
static GCConVar cv_debug_steam_startplaying( "cv_debug_steam_startplaying", "0", 0, "Turn this ON to debug the stream of startplaying messages we get from Steam" );
static GCConVar temp_list_mismatched_replies( "temp_list_mismatched_replies", "0", "When set to 1, this report all replies that fail because the incoming message didn't expect a response. Temporary to help track down some failed state" );
static GCConVar writeback_queue_max_accumulate_time( "writeback_queue_max_accumulate_time", "10", 0, "The maximum amount of time in seconds that the writeback queue will accumulate database writes before performing queries. This is the time *before* the queries are executed, which is unbounded." );
static GCConVar writeback_queue_max_caches( "writeback_queue_max_caches", "0", 0, "The maximum amount of caches to write back in a single transaction. Set to zero to remove this restriction." );
static GCConVar geolocation_spewlevel( "geolocation_spewlevel", "4", 0, "Spewlevel to use for geolocation debug spew" );
static GCConVar geolocation_loglevel( "geolocation_loglevel", "4", 0, "Spewlevel to use for geolocation debug spew" );
extern GCConVar max_user_messages_per_second;
// There is also a GCConVar writeback_delay to control how frequently we do writebacks.
// !KLUDGE! Temp shim. Will get rid of this when we bring over the real gcinterface stuff from DOTA.
CGCInterface g_GCInterface;
CGCInterface *GGCInterface() { return &g_GCInterface; }
CSteamID CGCInterface::ConstructSteamIDForClient( AccountID_t unAccountID ) const
{
return CSteamID( unAccountID, GetUniverse(), k_EAccountTypeIndividual );
}
//-----------------------------------------------------------------------------
// Purpose: Overrides the spew func used by Msg and DMsg to print to the console
//-----------------------------------------------------------------------------
SpewRetval_t ConsoleSpewFunc( SpewType_t type, const tchar *pMsg )
{
const char *fmt = ( sizeof( tchar ) == sizeof( char ) ) ? "%hs" : "%ls";
switch (type )
{
default:
case SPEW_MESSAGE:
case SPEW_LOG:
EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, fmt, pMsg );
break;
case SPEW_WARNING:
EmitWarning( SPEW_CONSOLE, SPEW_ALWAYS, fmt, pMsg );
break;
case SPEW_ASSERT:
if ( ThreadInMainThread() && ( g_pJobCur != NULL ) )
{
fmt = ( sizeof( tchar ) == sizeof( char ) ) ? "[Job %s] %hs" : "[Job %s] %ls";
EmitError( SPEW_CONSOLE, fmt, g_pJobCur->GetName(), pMsg );
}
else
{
EmitError( SPEW_CONSOLE, fmt, pMsg );
}
break;
case SPEW_ERROR:
EmitError( SPEW_CONSOLE, fmt, pMsg );
break;
}
if ( type == SPEW_ASSERT )
{
#ifndef WIN32
// Non-win32
bool bRaiseOnAssert = getenv( "RAISE_ON_ASSERT" ) || !!CommandLine()->FindParm( "-raiseonassert" );
#elif defined( _DEBUG )
// Win32 debug
bool bRaiseOnAssert = true;
#else
// Win32 release
bool bRaiseOnAssert = !!CommandLine()->FindParm( "-raiseonassert" );
#endif
return bRaiseOnAssert ? SPEW_DEBUGGER : SPEW_CONTINUE;
}
else if ( type == SPEW_ERROR )
return SPEW_ABORT;
else
return SPEW_CONTINUE;
}
class CGCShutdownJob : public CGCJob
{
public:
CGCShutdownJob( CGCBase *pGC ) : CGCJob( pGC ) {}
virtual bool BYieldingRunGCJob()
{
m_pGC->SetIsShuttingDown();
// Log off all of the game servers and users, so that if something
// in the log off dirties caches they can be written back
CUtlVector<CSteamID> vecIDsToStop;
for( CGCGSSession **ppSession = m_pGC->GetFirstGSSession(); ppSession != NULL; ppSession = m_pGC->GetNextGSSession( ppSession ) )
{
vecIDsToStop.AddToTail( (*ppSession)->GetSteamID() );
}
FOR_EACH_VEC( vecIDsToStop, i )
{
m_pGC->YieldingStopGameserver( vecIDsToStop[i] );
ShouldNotHoldAnyLocks();
}
vecIDsToStop.RemoveAll();
for( CGCUserSession **ppSession = m_pGC->GetFirstUserSession(); ppSession != NULL; ppSession = m_pGC->GetNextUserSession( ppSession ) )
{
vecIDsToStop.AddToTail( (*ppSession)->GetSteamID() );
}
FOR_EACH_VEC( vecIDsToStop, i )
{
m_pGC->YieldingStopPlaying( vecIDsToStop[i] );
ShouldNotHoldAnyLocks();
}
// wait for jobs to finish (except this one!)
const int kMaxIterations = 100;
int cIter = 0;
while ( cIter++ < kMaxIterations && m_pGC->GetJobMgr().CountJobs() > 1 )
{
BYieldingWaitOneFrame();
}
m_pGC->YieldingGracefulShutdown();
GGCHost()->ShutdownComplete();
return false;
}
};
class CPreTestSetupJob : public CGCJob
{
public:
CPreTestSetupJob( CGCBase *pGC ) : CGCJob( pGC ) {}
virtual bool BYieldingRunGCJob( GCSDK::CNetPacket *pNetPacket )
{
CGCMsg<MsgGCEmpty_t> msg( pNetPacket );
m_pGC->YieldingPreTestSetup();
return true;
}
};
GC_REG_JOB( CGCBase, CPreTestSetupJob, "CPreTestSetupJob", k_EGCMsgPreTestSetup, k_EServerTypeGC );
static void SpewSerializedKeyValues( const byte *pubVarData, uint32 cubVarData )
{
if ( pubVarData == NULL || cubVarData == 0 )
{
EmitInfo( SPEW_GC, 1, 1, " No KV data\n" );
return;
}
char szLine[512] = "";
for ( uint32 i = 0 ; i < cubVarData ; ++i )
{
char szByteVal[32];
V_sprintf_safe( szByteVal, "%02X", pubVarData[ i ] );
if ( i % 32 )
{
V_strcat_safe( szLine, ", " );
V_strcat_safe( szLine, szByteVal );
}
else
{
if ( szLine[0] )
EmitInfo( SPEW_GC, 1, 1, " %s\n", szLine );
V_strcpy_safe( szLine, szByteVal );
}
}
if ( szLine[0] )
EmitInfo( SPEW_GC, 1, 1, " %s\n", szLine );
KeyValuesAD pkvDetails( "SessionDetails" );
CUtlBuffer buf;
buf.Put( pubVarData, cubVarData );
if( pkvDetails->ReadAsBinary( buf ) )
{
FOR_EACH_VALUE( pkvDetails, v )
{
EmitInfo( SPEW_GC, 1, 1, " %s = %s\n", v->GetName(), v->GetString( NULL, "??" ) );
}
}
else
{
EmitInfo( SPEW_GC, 1, 1, " KV data failed parse\n" );
}
}
class CStartPlayingJob : public CGCJob
{
public:
CStartPlayingJob( CGCBase *pGC ) : CGCJob( pGC ) {}
virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
{
CGCMsg<MsgGCStartPlaying_t> msg( pNetPacket );
// @note Tom Bui/Joe Ludwig: This can happen for PS3 Steam accounts
if ( !msg.Body().m_steamID.IsValid() )
return true;
if ( cv_debug_steam_startplaying.GetBool() )
{
netadr_t serverAdr( msg.Body().m_unServerAddr, msg.Body().m_usServerPort );
EmitInfo( SPEW_GC, 1, 1, "Received StartPlaying( user = %s, GS = %s @ %s )\n", msg.Body().m_steamID.Render(), msg.Body().m_steamIDGS.Render(), serverAdr.ToString() );
SpewSerializedKeyValues( msg.PubVarData(), msg.CubVarData() );
}
m_pGC->QueueStartPlaying( msg.Body().m_steamID, msg.Body().m_steamIDGS, msg.Body().m_unServerAddr, msg.Body().m_usServerPort, msg.PubVarData(), msg.CubVarData() );
return true;
}
};
GC_REG_JOB(CGCBase, CStartPlayingJob, "CStartPlayingJob", k_EGCMsgStartPlaying, k_EServerTypeGC);
class CExecuteStartPlayingJob : public CGCJob
{
public:
CExecuteStartPlayingJob( CGCBase *pGC ) : CGCJob( pGC ) {}
virtual bool BYieldingRunGCJob( )
{
m_pGC->YieldingExecuteNextStartPlaying();
return true;
}
};
class CStopPlayingJob : public CGCJob
{
public:
CStopPlayingJob( CGCBase *pGC ) : CGCJob( pGC ) {}
virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
{
CGCMsg<MsgGCStopSession_t> msg( pNetPacket );
// @note Tom Bui/Joe Ludwig: This can happen for PS3 Steam accounts
if ( !msg.Body().m_steamID.IsValid() )
return true;
if ( cv_debug_steam_startplaying.GetBool() )
{
EmitInfo( SPEW_GC, 1, 1, "Received StopPlaying( user = %s )\n", msg.Body().m_steamID.Render() );
}
m_pGC->YieldingStopPlaying( msg.Body().m_steamID );
return true;
}
};
GC_REG_JOB(CGCBase, CStopPlayingJob, "CStopPlayingJob", k_EGCMsgStopPlaying, k_EServerTypeGC);
class CStartGameserverJob : public CGCJob
{
public:
CStartGameserverJob( CGCBase *pGC ) : CGCJob( pGC ) {}
virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
{
CGCMsg<MsgGCStartGameserver_t> msg( pNetPacket );
m_pGC->QueueStartPlaying( msg.Body().m_steamID, CSteamID(), msg.Body().m_unServerAddr, msg.Body().m_usServerPort, msg.PubVarData(), msg.CubVarData() );
return true;
}
};
GC_REG_JOB(CGCBase, CStartGameserverJob, "CStartGameserverJob", k_EGCMsgStartGameserver, k_EServerTypeGC);
class CStopGameserverJob : public CGCJob
{
public:
CStopGameserverJob( CGCBase *pGC ) : CGCJob( pGC ) {}
virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
{
CGCMsg<MsgGCStopSession_t> msg( pNetPacket );
m_pGC->YieldingStopGameserver( msg.Body().m_steamID );
return true;
}
};
GC_REG_JOB(CGCBase, CStopGameserverJob, "CStopGameserverJob", k_EGCMsgStopGameserver, k_EServerTypeGC);
class CGetSystemStatsJob : public CGCJob
{
public:
CGetSystemStatsJob( CGCBase *pGC ) : CGCJob( pGC ) {}
virtual bool BYieldingRunGCJob( GCSDK::IMsgNetPacket *pNetPacket )
{
CProtoBufMsg<CGCMsgGetSystemStats> msg( pNetPacket );
CProtoBufMsg<CGCMsgGetSystemStatsResponse> msgResponse( k_EGCMsgGetSystemStatsResponse );
msgResponse.Body().set_gc_app_id( m_pGC->GetAppID() );
// @note Tom Bui: we don't support dynamic stats yet, but once we do, we can use the KV stuff
m_pGC->SystemStats_Update( msgResponse.Body() );
// KVPacker packer;
// KeyValuesAD pKVStats( "GCStats" );
// CUtlBuffer buffer;
// if ( packer.WriteAsBinary( pKVStats, buffer ) )
// {
// msgResponse.Body().set_stats_kv( buffer.Base(), buffer.TellPut() );
// }
return m_pGC->BSendSystemMessage( msgResponse );
}
};
GC_REG_JOB(CGCBase, CGetSystemStatsJob, "CGetSystemStatsJob", k_EGCMsgGetSystemStats, k_EServerTypeGC);
//-----------------------------------------------------------------------------
class CGCJobAccountVacStatusChange : public CGCJob
{
public:
CGCJobAccountVacStatusChange( CGCBase *pGC ) : CGCJob( pGC ) {}
bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
{
CProtoBufMsg<CMsgGCHAccountVacStatusChange> msg( pNetPacket );
if ( GGCBase()->GetAppID() != msg.Body().app_id() )
return true;
CSteamID steamID( msg.Body().steam_id() );
bool bIsVacBanned = msg.Body().is_banned_now();
// Fetch app details, but force them to be re-loaded
bool bForceReload = true;
const CAccountDetails *pAccountDetails = GGCBase()->YieldingGetAccountDetails( steamID, bForceReload );
// Account details is up to date so just return
if ( pAccountDetails && bIsVacBanned != pAccountDetails->BIsVacBanned() )
{
EmitWarning( SPEW_GC, 2, "VAC status didn't update for %s afetr receiving VacStatusChange and the force reloading the account details\n", steamID.Render() );
}
return true;
}
};
GC_REG_JOB( CGCBase, CGCJobAccountVacStatusChange, "CGCJobAccountVacStatusChange", k_EGCMsgGCAccountVacStatusChange, k_EServerTypeGC );
//-----------------------------------------------------------------------------
class CGCJobAccountPhoneNumberChange : public CGCJob
{
public:
CGCJobAccountPhoneNumberChange( CGCBase *pGC ) : CGCJob( pGC ) {}
bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
{
CProtoBufMsg<CMsgGCHAccountPhoneNumberChange> msg( pNetPacket );
if ( GGCBase()->GetAppID() != msg.Body().appid() )
return true;
CSteamID steamID( msg.Body().steamid() );
CScopedSteamIDLock scopedLock( steamID );
if ( !scopedLock.BYieldingPerformLock( __FILE__, __LINE__ ) )
{
EmitError( SPEW_GC, __FUNCTION__ ": Failed to lock steamid %s\n", steamID.Render() );
return true;
}
bool bHasPhoneVerified = msg.Body().is_verified();
bool bIsPhoneIdentifying = msg.Body().is_identifying();
// Fetch app details, but force them to be re-loaded
bool bForceReload = true;
const CAccountDetails *pAccountDetails = GGCBase()->YieldingGetAccountDetails( steamID, bForceReload );
// Account details is up to date so just return
if ( pAccountDetails && ( bHasPhoneVerified != pAccountDetails->BIsPhoneVerified() ||
bIsPhoneIdentifying != pAccountDetails->BIsPhoneIdentifying() ) )
{
EmitWarning( SPEW_GC, 2, "Phone status didn't update for %s afetr receiving PhoneNumberChange and force reloading the account details\n",
steamID.Render() );
}
GGCBase()->YldOnAccountPhoneVerificationChange( steamID );
EmitInfo( SPEW_GC, 5, 5, "AccountPhoneVerificationChange for %s\n", steamID.Render() );
return true;
}
};
GC_REG_JOB( CGCBase, CGCJobAccountPhoneNumberChange, "CGCJobAccountPhoneNumberChange", k_EGCMsgAccountPhoneNumberChange, k_EServerTypeGC );
//-----------------------------------------------------------------------------
class CGCJobAccountTwoFactorChange : public CGCJob
{
public:
CGCJobAccountTwoFactorChange( CGCBase *pGC ) : CGCJob( pGC ) {}
bool BYieldingRunJobFromMsg( IMsgNetPacket *pNetPacket )
{
CProtoBufMsg<CMsgGCHAccountTwoFactorChange> msg( pNetPacket );
if ( GGCBase()->GetAppID() != msg.Body().appid() )
return true;
CSteamID steamID( msg.Body().steamid() );
bool bHasTwoFactor = msg.Body().twofactor_enabled();
// Fetch app details, but force them to be re-loaded
bool bForceReload = true;
const CAccountDetails *pAccountDetails = GGCBase()->YieldingGetAccountDetails( steamID, bForceReload );
// Account details is up to date so just return
if ( pAccountDetails && bHasTwoFactor != pAccountDetails->BIsTwoFactorAuthEnabled() )
{
EmitWarning( SPEW_GC, 2, "VAC status didn't update for %s afetr receiving VacStatusChange and the force reloading the account details\n", steamID.Render() );
}
GGCBase()->YldOnAccountTwoFactorChange( steamID );
EmitInfo( SPEW_GC, 5, 5, "AccountTwoFactorChange for %s\n", steamID.Render() );
return true;
}
};
GC_REG_JOB( CGCBase, CGCJobAccountTwoFactorChange, "CGCJobAccountTwoFactorChange", k_EGCMsgAccountTwoFactorChange, k_EServerTypeGC );
//-----------------------------------------------------------------------------
// Purpose: Constructor
//-----------------------------------------------------------------------------
CGCBase::CGCBase( )
: m_mapSOCache( ),
m_rbtreeSOCachesBeingLoaded( DefLessFunc( CSteamID ) ),
m_rbtreeSOCachesWithDirtyVersions( DefLessFunc( CSteamID ) ),
m_hashUserSessions( k_nUserSessionRunInterval/ k_cMicroSecPerShellFrame ),
m_hashGSSessions( k_nGSSessionRunInterval/ k_cMicroSecPerShellFrame ),
m_hashSteamIDLocks( k_nLocksRunInterval / k_cMicroSecPerShellFrame ),
m_bStartupComplete( false ),
m_bIsShuttingDown( false ),
m_bStartProfiling( false ),
m_bStopProfiling( false ),
m_bDumpVprofImbalances( false ),
m_nStartPlayingJobCount( 0 ),
m_nRequestSessionJobsActive( 0 ),
m_nLogonSurgeFramesRemaining( k_nMillion * 10 / k_cMicroSecPerShellFrame ), // stay in "logon surge" mode for at least 10 seconds after boot.
m_mapStartPlayingQueueIndexBySteamID( DefLessFunc( CSteamID ) ),
m_MsgRateLimit( max_user_messages_per_second ),
m_nStartupCompleteTime( CRTime::RTime32TimeCur() ),
m_nInitTime( CRTime::RTime32TimeCur() ),
m_jobidFlushInventoryCacheAccounts( k_GIDNil ),
m_numFlushInventoryCacheAccountsLastScheduled( 0 )
{
}
//-----------------------------------------------------------------------------
// Purpose: Destructor
//-----------------------------------------------------------------------------
CGCBase::~CGCBase()
{
}
//-----------------------------------------------------------------------------
// Purpose: Remembers the app ID and host
//-----------------------------------------------------------------------------
bool CGCBase::BInit( AppId_t unAppID, const char *pchAppPath, IGameCoordinatorHost *pHost )
{
VPROF_BUDGET( "CGCBase::BInit", VPROF_BUDGETGROUP_STEAM );
// Make sure we can't deploy debug GCs outside the dev environment
#ifdef _DEBUG
if ( pHost->GetUniverse() != k_EUniverseDev )
{
//pHost->EmitMessage( SPEW_GC, SPEW_ERROR, SPEW_ALWAYS, LOG_ALWAYS,
// CFmtStr( "The GC for App %u is a debug binary. Shutting down.\n", unAppID ) );
//return false;
pHost->EmitMessage( SPEW_GC.GetName(), SPEW_WARNING, SPEW_ALWAYS, LOG_ALWAYS,
CFmtStr( "The GC for App %u is a debug binary.\n", unAppID ) );
}
#endif
m_JobMgr.SetThreadPoolSize( GetThreadPoolSizeFromConVar() );
MsgRegistrationFromEnumDescriptor( EGCSystemMsg_descriptor(), GCSDK::MT_GC_SYSTEM );
MsgRegistrationFromEnumDescriptor( EGCBaseClientMsg_descriptor(), GCSDK::MT_GC );
MsgRegistrationFromEnumDescriptor( EGCToGCMsg_descriptor(), GCSDK::MT_GC_SYSTEM );
m_unAppID = unAppID;
m_pHost = pHost;
m_sPath = pchAppPath;
SetGCHost( pHost );
g_pGCBase = this;
SetMinidumpFilenamePrefix( CFmtStr("dumps\\gc%d", m_unAppID) );
// Make sure the assert dialog doesn't come up and hang the process in production
//SetAssertDialogDisabled( pHost->GetUniverse() != k_EUniverseDev );
SetAssertFailedNotifyFunc( CGCBase::AssertCallbackFunc );
// init the time very early so CRTime::RTime32TimeCur will return the right thing
CRTime::UpdateRealTime();
m_hashUserSessions.Init( k_cGCUserSessionInit, k_cBucketGCUserSession );
m_hashGSSessions.Init( k_cGCGSSessionInit, k_cBucketGCGSSession );
m_hashSteamIDLocks.Init( k_cGCLocksInit, k_cBucketGCLocks );
m_OutputFuncPrev = GetSpewOutputFunc();
SpewOutputFunc( &ConsoleSpewFunc );
EmitInfo( SPEW_GC, 1, 1, "CGCBase::BInit( AppID=%d, appPath=%s, sPath=%s )\n", unAppID, pchAppPath, m_sPath.String() );
if ( !OnInit() )
return false;
DbgVerify( g_theMessageList.BInit( ) );
/*
// @note Tom Bui: we don't need dynamic stats...yet.
// when we do, we'll need to specify the how the values are aggregated over all the same GCs
// and how the values should be treated
KeyValuesAD pKVStats( "GCStats" );
SystemStats_Update( pKVStats );
CUtlBuffer buffer;
KVPacker packer;
if ( packer.WriteAsBinary( pKVStats, buffer ) )
{
CProtoBufMsg< CGCMsgSystemStatsSchema > msg( GCSDK::k_EGCMsgSystemStatsSchema );
msg.Body().set_gc_app_id( GetAppID() );
msg.Body().set_schema_kv( buffer.Base(), buffer.TellPut() );
BSendSystemMessage( msg );
}
*/
return BSendWebApiRegistration();
}
//-----------------------------------------------------------------------------
// Purpose: Report back to the host that startup is complete
//-----------------------------------------------------------------------------
void CGCBase::SetStartupComplete( bool bSuccess )
{
// !KLUDGE! Fatal error messages on startup frequently get lost in the
// mass of messages. Let's spray a big error message box if we fail
// to startup. Ideally, the cause of the failure will be
// spewed just above this box.
if ( !bSuccess )
{
EmitError( SPEW_GC, "^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^\n" );
EmitError( SPEW_GC, "GC failed to startup. Error mesage is probably directly above\n" );
EmitError( SPEW_GC, "**************************************************************\n" );
}
m_nStartupCompleteTime = CRTime::RTime32TimeCur();
m_bStartupComplete = true;
GGCHost()->StartupComplete( bSuccess );
}
uint32 CGCBase::GetGCUpTime() const
{
return CRTime::RTime32TimeCur() - m_nInitTime;
}
//-----------------------------------------------------------------------------
// Purpose: Starts a job to perform graceful shutdown
//-----------------------------------------------------------------------------
void CGCBase::Shutdown()
{
VPROF_BUDGET( "CGCBase::Shutdown", VPROF_BUDGETGROUP_STEAM );
m_DumpHTTPErrorsSchedule.Cancel();
CGCShutdownJob *pJob = new CGCShutdownJob( this );
pJob->StartJob( NULL );
}
//-----------------------------------------------------------------------------
// Purpose: Cleans up the GC to prepare for shutdown
//-----------------------------------------------------------------------------
void CGCBase::Uninit( )
{
VPROF_BUDGET( "CGCBase::Uninit", VPROF_BUDGETGROUP_STEAM );
OnUninit();
// clean up all of the sessions and caches here so we can be sure it happens before the memory pools go away at static destruction time
for( CGCUserSession **ppSession = m_hashUserSessions.PvRecordFirst(); ppSession != NULL; ppSession = m_hashUserSessions.PvRecordNext( ppSession ) )
{
delete (*ppSession);
}
m_hashUserSessions.RemoveAll();
for( CGCGSSession **ppSession = m_hashGSSessions.PvRecordFirst(); ppSession != NULL; ppSession = m_hashGSSessions.PvRecordNext( ppSession ) )
{
delete (*ppSession);
}
m_hashGSSessions.RemoveAll();
FOR_EACH_MAP_FAST( m_mapSOCache, nIndex )
{
// Remove from map before deleting, to prevent some debug
// code from getting tangled up
CGCSharedObjectCache *pCache = m_mapSOCache[nIndex];
m_mapSOCache[nIndex] = NULL;
m_mapSOCache.RemoveAt( nIndex );
delete pCache;
}
m_mapSOCache.RemoveAll();
m_rbtreeSOCachesBeingLoaded.RemoveAll();
m_rbtreeSOCachesWithDirtyVersions.RemoveAll();
m_hashSteamIDLocks.RemoveAll();
GSchemaFull().Uninit();
SpewOutputFunc( m_OutputFuncPrev );
}
GCConVar cv_flush_inventory_cache_jobs( "cv_flush_inventory_cache_jobs", "20", 0, "The maximum number of jobs flushing inventory caches that can be in flight at once, zero to disable flushing" );
GCConVar cv_flush_inventory_cache_contextid( "cv_flush_inventory_cache_contextid", "2" /* k_EEconContextBackpack */, 0, "Which context id we flush for Steam web user-facing inventory" );
GCConVar cv_flush_inventory_cache_spew( "cv_flush_inventory_cache_spew", "0", 0, "Controls spew level for jobs flushing inventory cache (0=off; 1=summary; 2=verbose)" );
class CFlushInventoryCacheAccountsJob : public CGCJob, public IYieldingParallelFarmJobHandler
{
public:
CFlushInventoryCacheAccountsJob( CGCBase *pGC, CUtlRBTree< AccountID_t, int32, CDefLess< AccountID_t > > &rbAccounts ) : CGCJob( pGC )
{
m_rbAccounts.Swap( rbAccounts );
}
virtual bool BYieldingRunGCJob() OVERRIDE
{
if ( !m_rbAccounts.Count() )
return false;
if ( cv_flush_inventory_cache_jobs.GetInt() <= 0 )
return false;
bool bShouldSpew = ( cv_flush_inventory_cache_spew.GetInt() >= 1 );
uint32 msTimeStart = 0;
int numAccountsWorkload = m_rbAccounts.Count();
if ( bShouldSpew )
{
msTimeStart = Plat_MSTime();
}
{ // Run parallel processing of the workload
int numJobs = numAccountsWorkload;
numJobs = MIN( cv_flush_inventory_cache_jobs.GetInt(), numJobs );
numJobs = MAX( 1, numJobs );
( void ) BYieldingExecuteParallel( numJobs, "YieldingFlushInventoryCacheAccountsJob" );
}
if ( bShouldSpew )
{
EmitInfo( SPEW_GC, SPEW_ALWAYS, LOG_ALWAYS, "IEconService/FlushInventoryCache: Batch for %d accounts completed in %u ms\n",
numAccountsWorkload, Plat_MSTime() - msTimeStart );
}
return true;
}
virtual bool BYieldingRunWorkload( int iJobSequenceCounter, bool *pbWorkloadCompleted ) OVERRIDE
{
if ( m_rbAccounts.Count() )
{
int32 idxElement = m_rbAccounts.FirstInorder();
AccountID_t unAccountID = m_rbAccounts.Element( idxElement );
m_rbAccounts.RemoveAt( idxElement );
( void ) BYieldingFlushRequest( unAccountID );
}
if ( !m_rbAccounts.Count() )
{
*pbWorkloadCompleted = true;
}
return true;
}
bool BYieldingFlushRequest( AccountID_t unAccountID )
{
bool bShouldSpew = ( cv_flush_inventory_cache_spew.GetInt() >= 2 );
uint32 msTimeStart = 0;
if ( bShouldSpew )
{
msTimeStart = Plat_MSTime();
}
CSteamID steamID( GGCInterface()->ConstructSteamIDForClient( unAccountID ) );
CSteamAPIRequest apiRequest( k_EHTTPMethodPOST, "IEconService", "FlushInventoryCache", 1 );
apiRequest.SetPOSTParamUInt32( "appid", GGCBase()->GetAppID() );
apiRequest.SetPOSTParamUInt64( "steamid", steamID.ConvertToUint64() );
apiRequest.SetPOSTParamUInt32( "contextid", 2 );
CHTTPResponse apiResponse;
bool bSucceededQuery = m_pGC->BYieldingSendHTTPRequest( &apiRequest, &apiResponse );
if ( !bSucceededQuery )
{
EmitErrorRatelimited( SPEW_GC, "IEconService/FlushInventoryCache: Web call did not get a response for %s.\n", steamID.Render() );
}
else if ( k_EHTTPStatusCode200OK != apiResponse.GetStatusCode() )
{
EmitErrorRatelimited( SPEW_GC, "IEconService/FlushInventoryCache: Web call got failure code %d for %s\n", apiResponse.GetStatusCode(), steamID.Render() );
bSucceededQuery = false;
}
if ( bSucceededQuery )
{
// Have a valid response
KeyValuesAD pKVResponse( "response" );
pKVResponse->UsesEscapeSequences( true );
if ( !pKVResponse->LoadFromBuffer( "webResponse", *apiResponse.GetBodyBuffer() ) )
{
EmitErrorRatelimited( SPEW_GC, "IEconService/FlushInventoryCache: Web call got code %d for %s, but failed to parse response\n", apiResponse.GetStatusCode(), steamID.Render() );
bSucceededQuery = false;
}
else if ( !pKVResponse->GetBool( "success" ) )
{
// We got a response, and it's not success
EmitErrorRatelimited( SPEW_GC, "IEconService/FlushInventoryCache: Web call got code %d for %s, but not success\n", apiResponse.GetStatusCode(), steamID.Render() );
bSucceededQuery = false;
}
}
if ( bShouldSpew )
{
EmitInfo( SPEW_GC, SPEW_ALWAYS, LOG_ALWAYS, "IEconService/FlushInventoryCache: Web call for %s %s in %u ms\n",
steamID.Render(), bSucceededQuery ? "succeeded" : "failed", Plat_MSTime() - msTimeStart );
}
return bSucceededQuery;
}
public:
CUtlRBTree< AccountID_t, int32, CDefLess< AccountID_t > > m_rbAccounts;
};
//-----------------------------------------------------------------------------
// Purpose: Called every frame. Mostly updates times and pulses the job manager
//-----------------------------------------------------------------------------
bool CGCBase::BMainLoopOncePerFrame( uint64 ulLimitMicroseconds )
{
// if we don't have a GCHost yet, don't do any work per frame
if( !GGCHost() )
return false;
#ifndef STEAM
CRTime::UpdateRealTime();
#endif
#ifdef VPROF_ENABLED
// Make sure we end the frame at the root node
if ( !g_VProfCurrentProfile.AtRoot() && m_bDumpVprofImbalances )
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "VProf not at root at end of frame. Stack:\n" );
}
for( int i = 0; !g_VProfCurrentProfile.AtRoot() && i < 100; i++ )
{
if ( m_bDumpVprofImbalances )
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, " %s\n", g_VProfCurrentProfile.GetCurrentNode()->GetName() );
}
g_VProfCurrentProfile.ExitScope();
}
g_VProfCurrentProfile.MarkFrame();
if ( m_bStopProfiling || m_bStartProfiling )
{
while ( g_VProfCurrentProfile.IsEnabled() )
{
g_VProfCurrentProfile.Stop();
}
m_bStopProfiling = false;
if ( m_bStartProfiling )
{
g_VProfCurrentProfile.Reset();
g_VProfCurrentProfile.Start();
m_bStartProfiling = false;
}
}
#endif
VPROF_BUDGET( "Main Loop", VPROF_BUDGETGROUP_STEAM );
CLimitTimer limitTimer;
limitTimer.SetLimit( ulLimitMicroseconds );
CJobTime::UpdateJobTime( k_cMicroSecPerShellFrame );
bool bWorkRemaining = m_JobMgr.BFrameFuncRunSleepingJobs( limitTimer );
//run all of our frame functions
GFrameFunctionMgr().RunFrame( limitTimer );
{
VPROF_BUDGET( "Run Sessions", VPROF_BUDGETGROUP_STEAM );
m_AccountDetailsManager.MarkFrame();
m_hashUserSessions.StartFrameSchedule( true );
m_hashGSSessions.StartFrameSchedule( true );
m_hashSteamIDLocks.StartFrameSchedule( true );
bool bUsersFinished = false, bGSFinished = false;
while( !limitTimer.BLimitReached() && ( !bUsersFinished || !bGSFinished ) )
{
if( !bUsersFinished )
{
CGCUserSession **ppSession = m_hashUserSessions.PvRecordRun();
if ( ppSession && *ppSession )
{
(*ppSession)->Run();
}
else
{
bUsersFinished = true;
}
if ( m_hashUserSessions.BCompletedPass() )
{
FinishedMainLoopUserSweep();
}
}
if( !bGSFinished )
{
CGCGSSession **ppSession = m_hashGSSessions.PvRecordRun();
if ( ppSession && *ppSession )
{
(*ppSession)->Run();
}
else
{
bGSFinished = true;
}
}
}
}
{
VPROF_BUDGET( "UpdateSOCacheVersions", VPROF_BUDGETGROUP_STEAM );
UpdateSOCacheVersions();
}
if( m_llStartPlaying.Count() > 0 )
{
VPROF_BUDGET( "StartStartPlayingJobs", VPROF_BUDGETGROUP_STEAM );
int nJobsNeeded = min( m_llStartPlaying.Count(), cv_concurrent_start_playing_limit.GetInt() - m_nStartPlayingJobCount );
while( nJobsNeeded > 0 )
{
nJobsNeeded--;
m_nStartPlayingJobCount++;
CExecuteStartPlayingJob *pJob = new CExecuteStartPlayingJob( this );
pJob->StartJob( NULL );
}
}
// Decide if we should be in logon surge
bool bShouldBeInlogonSurge =
m_llStartPlaying.Count() >= cv_logon_surge_start_playing_limit.GetInt();
// This might be a good idea, but let's see what the real numbers are during logon surge.
//|| m_nRequestSessionJobsActive >= cv_logon_surge_request_session_jobs.GetInt();
// Check if we're already in logon surge, is it time to check if we should leave,
// and should we dump our status periodically?
const int k_nLogonSurgeFrameInterval = k_nMillion * 10 / k_cMicroSecPerShellFrame;
if ( m_nLogonSurgeFramesRemaining > 0 )
{
// Currently in logon surge
--m_nLogonSurgeFramesRemaining;
if ( m_nLogonSurgeFramesRemaining == 0 )
{
// Time to check for leaving logon surge mode.
// Should I flip the flag off?
if ( bShouldBeInlogonSurge )
{
// We're still in logon surge. Schedule another check
// a few frames from now, and dump our status.
m_nLogonSurgeFramesRemaining = k_nLogonSurgeFrameInterval;
Dump();
}
else
{
// We're over the hump!
EmitInfo( SPEW_GC, SPEW_ALWAYS, LOG_ALWAYS, "** LOGON SURGE COMPLETED **\n" );
}
}
}
else if ( bShouldBeInlogonSurge )
{
// We finished logon surge one, but now we are re-entering it.
// This usually doesn't happen. This is suspicious.
EmitWarning( SPEW_GC, SPEW_ALWAYS, "RE-ENTERING logon surge mode!\n" );
m_nLogonSurgeFramesRemaining = k_nLogonSurgeFrameInterval;
}
else
{
// Not in logon surge. make sure flag is slammed to zero
m_nLogonSurgeFramesRemaining = 0;
}
// Flush inventory cache for accounts
if ( m_rbFlushInventoryCacheAccounts.Count() && ( ( m_jobidFlushInventoryCacheAccounts == k_GIDNil ) ||
!GetJobMgr().BJobExists( m_jobidFlushInventoryCacheAccounts ) ) )
{
m_numFlushInventoryCacheAccountsLastScheduled = m_rbFlushInventoryCacheAccounts.Count();
m_jobidFlushInventoryCacheAccounts = StartNewJobDelayed( new CFlushInventoryCacheAccountsJob( this, m_rbFlushInventoryCacheAccounts ) )->GetJobID();
}
bool bSubRet = OnMainLoopOncePerFrame( limitTimer );
return bWorkRemaining || bSubRet;
}
bool CGCBase::BShouldThrottleLowServiceLevelWebAPIJobs() const
{
// Always throttle them during logon surge.
if ( BIsInLogonSurge() )
return true;
// Check threshold
if ( m_JobMgr.CountJobs() > cv_webapi_throttle_job_threshold.GetInt() )
return true;
// We are not too busy, we can service the request
return false;
}
bool CGCBase::BMainLoopUntilFrameCompletion( uint64 ulLimitMicroseconds )
{
VPROF_BUDGET( "Main Loop", VPROF_BUDGETGROUP_STEAM );
CLimitTimer limitTimer;
limitTimer.SetLimit( ulLimitMicroseconds );
bool bRet = m_JobMgr.BFrameFuncRunYieldingJobs( limitTimer );
bRet |= GSDOCache().BFrameFuncRunJobsUntilCompleted( limitTimer );
bRet |= GSDOCache().BFrameFuncRunMemcachedQueriesUntilCompleted( limitTimer );
bRet |= GSDOCache().BFrameFuncRunSQLQueriesUntilCompleted( limitTimer );
bRet |= m_AccountDetailsManager.BExpireRecords( limitTimer );
bool bSubRet = OnMainLoopUntilFrameCompletion( limitTimer );
bRet |= GFrameFunctionMgr().RunFrameTick( limitTimer );
{
VPROF_BUDGET( "Expire locks", VPROF_BUDGETGROUP_STEAM );
for ( CLock *pLock = m_hashSteamIDLocks.PvRecordRun(); NULL != pLock; pLock = m_hashSteamIDLocks.PvRecordRun() )
{
if ( !pLock->BIsLocked() && pLock->GetMicroSecondsSinceLock() > k_cMicroSecLockLifetime )
{
m_hashSteamIDLocks.Remove( pLock );
}
if ( limitTimer.BLimitReached() )
return true;
}
}
return bRet || bSubRet;
}
//-----------------------------------------------------------------------------
// Purpose: Called when we get to the end of a user session Run() sweep, and
// are about to start over with the first session in the list.
//-----------------------------------------------------------------------------
void CGCBase::FinishedMainLoopUserSweep()
{
// Base class does nothing
}
//-----------------------------------------------------------------------------
// Purpose: Queues up a start playing request that we should process when we
// get a chance.
//-----------------------------------------------------------------------------
void CGCBase::QueueStartPlaying( const CSteamID & steamID, const CSteamID & gsSteamID, uint32 unServerAddr, uint16 usServerPort, const uint8 *pubVarData, uint32 cubVarData )
{
MEM_ALLOC_CREDIT_( "QueueStartPlaying" );
Assert( steamID.BIndividualAccount() || steamID.BGameServerAccount() );
Assert( steamID.IsValid() );
// Should be one-to-one correspondence in these data structures
Assert( (size_t)m_mapStartPlayingQueueIndexBySteamID.Count() == (size_t)m_llStartPlaying.Count() );
// !FIXME! Here we really should check whether they already have a session.
// if so, we've already gone through all the startplaying work and shouldn't
// repeat it. We might just need to kick the communications or make
// sure they are on the right game server.
// Check if we already have an entry in the queue for this guy.
StartPlayingWork_t *pWork = NULL;
int nMapIndex = m_mapStartPlayingQueueIndexBySteamID.Find( steamID );
if ( nMapIndex != m_mapStartPlayingQueueIndexBySteamID.InvalidIndex() )
{
// We already have an entry for this guy, let's update this one, rather than creating a new one
int nQueueIndex = m_mapStartPlayingQueueIndexBySteamID[ nMapIndex ];
pWork = &m_llStartPlaying[ nQueueIndex ];
// Sanity check data structures. I'd use an assert,
// but this is going live in an environment without
// asserts enabled, so I need to use spew.
if ( pWork->m_steamID == steamID )
{
// Don't leak user data, if we had any
delete pWork->m_pVarData;
pWork->m_pVarData = NULL;
// // This could definitely happen occasionally, but if it happens with massive frequency,
// // something is wrong
// if ( gsSteamID == pWork->m_gsSteamID )
// {
// EmitInfo( SPEW_GC, 4, LOG_ALWAYS, "Got StartPlaying message for %s, who was already in the startplaying queue for the same gameserver %s.\n", steamID.Render(), gsSteamID.Render() );
// }
// else
// {
// EmitInfo( SPEW_GC, 4, LOG_ALWAYS, "Got StartPlaying message for %s, who was already in the startplaying queue; changing gameserver %s -> %s.\n", steamID.Render(), pWork->m_gsSteamID.Render(), gsSteamID.Render() );
// }
}
else
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "Start playing queue corruption! Map entry points to wrong queue entry!\n" );
pWork = NULL;
m_mapStartPlayingQueueIndexBySteamID.RemoveAt( nMapIndex );
}
}
else
{
// EmitInfo( SPEW_GC, 4, LOG_ALWAYS, "Got StartPlaying message for %s, new queue for gameserver %s.\n", steamID.Render(), gsSteamID.Render() );
}
// Need to create a new entry?
if ( pWork == NULL )
{
// Create a new queue entry
int nQueueIndex = m_llStartPlaying.AddToTail();
pWork = &m_llStartPlaying[ nQueueIndex ];
// Add it to the steam ID map, so we can locate this guy quickly in the future
m_mapStartPlayingQueueIndexBySteamID.Insert( steamID, nQueueIndex );
}
// Fill in the queue entry with the latest details
pWork->m_steamID = steamID;
pWork->m_gsSteamID = gsSteamID;
pWork->m_unServerAddr = unServerAddr;
pWork->m_usServerPort = usServerPort;
if( cubVarData )
{
pWork->m_pVarData = new CUtlBuffer;
pWork->m_pVarData->Put( pubVarData, cubVarData );
}
else
{
pWork->m_pVarData = NULL;
}
// Should be one-to-one correspondence in these data structures
Assert( (size_t)m_mapStartPlayingQueueIndexBySteamID.Count() == (size_t)m_llStartPlaying.Count() );
}
//-----------------------------------------------------------------------------
bool CGCBase::BRemoveStartPlayingQueueEntry( const CSteamID & steamID )
{
int nMapIndex = m_mapStartPlayingQueueIndexBySteamID.Find( steamID );
if ( nMapIndex == m_mapStartPlayingQueueIndexBySteamID.InvalidIndex() )
{
return false;
}
//EmitInfo( SPEW_GC, 4, LOG_ALWAYS, "Removed startplaying queue entry for %s.\n", steamID.Render() );
// Locate queue entry, make sure it matches, and remote it
int nQueueIndex = m_mapStartPlayingQueueIndexBySteamID[ nMapIndex ];
if ( m_llStartPlaying[ nQueueIndex ].m_steamID == steamID )
{
delete m_llStartPlaying[ nQueueIndex ].m_pVarData;
m_llStartPlaying.Remove( nQueueIndex );
}
else
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "Start playing queue corruption! Map entry doesn't point to matching queue index (found while removing entry in BRemoveStartPlayingQueueEntry)!\n" );
}
// Remove from map
m_mapStartPlayingQueueIndexBySteamID.RemoveAt( nQueueIndex );
// Found and removed
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Pull the next startplaying job off the queue and executes it
//-----------------------------------------------------------------------------
void CGCBase::YieldingExecuteNextStartPlaying()
{
// maybe we have nothing to do!
if( m_llStartPlaying.Count() > 0 )
{
// Execute the entry at the head
YieldingExecuteStartPlayingQueueEntryByIndex( m_llStartPlaying.Head() );
}
m_nStartPlayingJobCount--;
}
//-----------------------------------------------------------------------------
// Purpose: Executes a single entry from the start playing queue, given the linked list handle
//-----------------------------------------------------------------------------
void CGCBase::YieldingExecuteStartPlayingQueueEntryByIndex( int idxStartPlayingQueue )
{
VPROF_BUDGET( "CGCBase::YieldingExecuteStartPlayingQueueEntryByIndex - LinkedList", VPROF_BUDGETGROUP_STEAM );
// Remove the entry from the queue
StartPlayingWork_t work = m_llStartPlaying[ idxStartPlayingQueue ];
m_llStartPlaying.Remove( idxStartPlayingQueue );
VPROF_BUDGET( "CGCBase::YieldingExecuteStartPlayingQueueEntryByIndex", VPROF_BUDGETGROUP_STEAM );
// Remove it from the Steam ID map, too.
int nMapIndex = m_mapStartPlayingQueueIndexBySteamID.Find( work.m_steamID );
if ( nMapIndex == m_mapStartPlayingQueueIndexBySteamID.InvalidIndex() )
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "Start playing queue corruption! Queue entry is not in map!\n" );
}
else if ( m_mapStartPlayingQueueIndexBySteamID[ nMapIndex ] != idxStartPlayingQueue )
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "Start playing queue corruption! Map entry doesn't have proper queue index!\n" );
}
else
{
m_mapStartPlayingQueueIndexBySteamID.RemoveAt( nMapIndex );
}
// Do the work.
if ( work.m_steamID.BIndividualAccount() )
{
YieldingStartPlaying( work.m_steamID, work.m_gsSteamID, work.m_unServerAddr, work.m_usServerPort, work.m_pVarData );
}
else if ( work.m_steamID.BGameServerAccount() )
{
const uint8 *pVarData = NULL;
uint32 cubVarData = 0;
if ( work.m_pVarData != NULL )
{
pVarData = (const uint8 *)work.m_pVarData->Base();
cubVarData = work.m_pVarData->TellMaxPut();
}
YieldingStartGameserver( work.m_steamID, work.m_unServerAddr, work.m_usServerPort, pVarData, cubVarData );
}
else
{
AssertMsg1( false, "Bogus steam ID %s in start playing queue", work.m_steamID.Render() );
}
// Clean up
delete work.m_pVarData;
}
void CGCBase::SetUserSessionDetails( CGCUserSession *pUserSession, KeyValues *pkvDetails )
{
if( pkvDetails )
{
pUserSession->m_unIPPublic = pkvDetails->GetInt( "ip", 0 );
pUserSession->m_osType = static_cast<EOSType>( pkvDetails->GetInt( "osType", k_eOSUnknown ) );
pUserSession->m_bIsTestSession = pkvDetails->GetInt( "isTestSession", 0 ) != 0;
pUserSession->m_bIsSecure = pkvDetails->GetInt( "secure", 0 ) != 0;
}
}
//-----------------------------------------------------------------------------
// Purpose: Does the real work when a player starts playing (inside a job)
//-----------------------------------------------------------------------------
void CGCBase::YieldingStartPlaying( const CSteamID & steamID, const CSteamID & gsSteamID, uint32 unServerAddr, uint16 usServerPort, CUtlBuffer *pVarData )
{
VPROF_BUDGET( "CGCBase::YieldingStartPlaying", VPROF_BUDGETGROUP_STEAM );
if ( m_bIsShuttingDown )
return;
if( !BYieldingLockSteamID( steamID, __FILE__, __LINE__ ) )
{
EmitError( SPEW_GC, "Failed to lock steamID %s in YieldingStartPlaying\n", steamID.Render() );
return;
}
// if var data came with this StartPlaying message, parse it into a KV and stick it on the session
KeyValues *pkvDetails = NULL;
if( pVarData )
{
MEM_ALLOC_CREDIT_("StartPlaying - SessionDetails" );
pkvDetails = new KeyValues( "SessionDetails" );
if( !pkvDetails->ReadAsBinary( *pVarData ) )
{
EmitError( SPEW_GC, "Unable to parse session details for %s\n", steamID.Render() );
pkvDetails->deleteThis();
pkvDetails = NULL;
}
}
CGCUserSession *pSession = FindUserSession( steamID );
if( !pSession )
{
// Load their SO cache. Remember, we already have their steam ID locked.
VPROF_BUDGET( "CGCBase::YieldingStartPlaying - Load SOCache", VPROF_BUDGETGROUP_STEAM );
CGCSharedObjectCache *pSOCache = YieldingFindOrLoadSOCache( steamID );
if ( !pSOCache )
{
EmitError( SPEW_GC, "Failed to get cache for user %s\n", steamID.Render() );
return;
}
// Create session of app-specific type
VPROF_BUDGET( "CGCBase::YieldingStartPlaying - CreateUserSession", VPROF_BUDGETGROUP_STEAM );
pSession = CreateUserSession( steamID, pSOCache );
if ( !pSession )
{
EmitError( SPEW_GC, "Failed to create user session for %s\n", steamID.Render() );
return;
}
VPROF_BUDGET( "CGCBase::YieldingStartPlaying - LRU Update", VPROF_BUDGETGROUP_STEAM );
RemoveCacheFromLRU( pSOCache );
CGCUserSession **ppSession = m_hashUserSessions.PvRecordInsert( steamID.ConvertToUint64() );
*ppSession = pSession;
SetUserSessionDetails( pSession, pkvDetails );
// Do game-specific logic here. Note that we're still holding the game server
// lock...
VPROF_BUDGET( "CGCBase::YieldingStartPlaying - Game-specific start playing", VPROF_BUDGETGROUP_STEAM );
YieldingSessionStartPlaying( pSession );
}
else if ( pSession->BIsShuttingDown() )
{
pkvDetails->deleteThis();
pkvDetails = NULL;
return;
}
else
{
// Update secure flag, etc from KV details, if any
SetUserSessionDetails( pSession, pkvDetails );
}
if ( pkvDetails )
{
pkvDetails->deleteThis();
pkvDetails = NULL;
}
VPROF_BUDGET( "CGCBase::YieldingStartPlaying - Game Server binding", VPROF_BUDGETGROUP_STEAM );
// Make sure the server exists and then try to join it
if ( gsSteamID.IsValid() && gsSteamID.BGameServerAccount() && BYieldingLockSteamID( gsSteamID, __FILE__, __LINE__ ) )
{
// First, try to obtain a session through ordinary means, by validating
// the session
if ( YieldingGetLockedGSSession( gsSteamID, __FILE__, __LINE__ ) != NULL )
{
// Maintain lock balance
UnlockSteamID( gsSteamID );
}
else
{
// Failed to get a session --- probably an AM is down.
// This is hopefully relatively rare, as it's not ideal.
// log it
if ( enable_startplaying_gameserver_creation_spew.GetBool() )
{
netadr_t serverAdr( unServerAddr, usServerPort );
EmitInfo( SPEW_GC, 2, LOG_ALWAYS, "Creating gameserver session %s @ %s as a result of user %s StartPlaying.\n", gsSteamID.Render(), serverAdr.ToString(), steamID.Render() );
}
YieldingFindOrCreateGSSession( gsSteamID, unServerAddr, usServerPort, NULL, 0 );
}
// Mark that we are joined to this server
pSession->BSetServer( gsSteamID );
// Done, clean up lock
UnlockSteamID( gsSteamID );
}
else
{
// Steam was sometimes sending us messages with zero Steam ID, even when we're on a server.
if ( cv_debug_steam_startplaying.GetBool() )
EmitInfo( SPEW_GC, 1, 1, "YieldingStartPlaying ( user = %s ) with invalid GS steam ID %s, calling LeaveServer\n", steamID.Render(), gsSteamID.Render() );
pSession->BLeaveServer();
}
}
//-----------------------------------------------------------------------------
// Purpose: Called when a player stops playing our game
//-----------------------------------------------------------------------------
void CGCBase::YieldingStopPlaying( const CSteamID & steamID )
{
// Should be one-to-one correspondence in these data structures
Assert( (size_t)m_mapStartPlayingQueueIndexBySteamID.Count() == (size_t)m_llStartPlaying.Count() );
// Check if they have an entry in the startplaying queue, then get rid of it!
BRemoveStartPlayingQueueEntry( steamID );
if ( !BLockSteamIDImmediate( steamID ) )
{
CGCUserSession *pSession = FindUserSession( steamID );
if ( !pSession )
{
return;
}
pSession->SetIsShuttingDown( true );
if( !BYieldingLockSteamID( steamID, __FILE__, __LINE__ ) )
{
EmitError( SPEW_GC, "Unable to lock steamID %s in YieldingStopPlaying\n", steamID.Render() );
return;
}
}
CGCUserSession *pSession = FindUserSession( steamID );
if( pSession )
{
pSession->BLeaveServer();
YieldingSessionStopPlaying( pSession );
if( pSession->GetSOCache() )
{
AddCacheToLRU( pSession->GetSOCache() );
}
m_hashUserSessions.Remove( steamID.ConvertToUint64() );
delete pSession;
}
// Clean up lock. Even if the session is gone and there's nothing
// for the lock to protect, we need this to avoid spurious asserts that check
// lock imbalance
UnlockSteamID( steamID );
}
//-----------------------------------------------------------------------------
// Purpose: Called when a gameserver stops running for our game
//-----------------------------------------------------------------------------
void CGCBase::YieldingStartGameserver( const CSteamID & steamID, uint32 unServerAddr, uint16 usServerPort, const uint8 *pubVarData, uint32 cubVarData )
{
VPROF_BUDGET( "CGCBase::YieldingStartGameserver", VPROF_BUDGETGROUP_STEAM );
if ( m_bIsShuttingDown )
return;
if( !BYieldingLockSteamID( steamID, __FILE__, __LINE__ ) )
{
EmitError( SPEW_GC, "Failed to lock steamID %s in YieldingStartGameserver\n", steamID.Render() );
return;
}
YieldingFindOrCreateGSSession( steamID, unServerAddr, usServerPort, pubVarData, cubVarData );
// Clean up
UnlockSteamID( steamID );
}
//-----------------------------------------------------------------------------
// Purpose: Called when a gameserver stops running for our game
//-----------------------------------------------------------------------------
void CGCBase::YieldingStopGameserver( const CSteamID & steamID )
{
// Should be one-to-one correspondence in these data structures
Assert( (size_t)m_mapStartPlayingQueueIndexBySteamID.Count() == (size_t)m_llStartPlaying.Count() );
// Check if they have an entry in the startplaying queue, then get rid of it!
BRemoveStartPlayingQueueEntry( steamID );
if ( !BLockSteamIDImmediate( steamID ) )
{
CGCGSSession *pSession = FindGSSession( steamID );
if ( !pSession )
{
return;
}
pSession->SetIsShuttingDown( true );
if( !BYieldingLockSteamID( steamID, __FILE__, __LINE__ ) )
{
EmitError( SPEW_GC, "Unable to lock steamID %s in YieldingStopGameserver\n", steamID.Render() );
return;
}
}
CGCGSSession *pSession = FindGSSession( steamID );
if( pSession )
{
pSession->RemoveAllUsers();
YieldingSessionStopServer( pSession );
if( pSession->GetSOCache() )
{
AddCacheToLRU( pSession->GetSOCache() );
}
m_hashGSSessions.Remove( steamID.ConvertToUint64() );
delete pSession;
}
// Clean up lock. Even if the session is gone and there's nothing
// for the lock to protect, we need this to avoid spurious asserts that check
// lock imbalance
UnlockSteamID( steamID );
}
IMsgNetPacket *CreateIMsgNetPacket( GCProtoBufMsgSrc eReplyType, const CSteamID senderID, uint32 nGCDirIndex, uint32 unMsgType, void *pubData, uint32 cubData )
{
VPROF_BUDGET( "CreateIMsgNetPacket", VPROF_BUDGETGROUP_STEAM );
if( 0 != ( unMsgType & k_EMsgProtoBufFlag ) )
{
if ( cubData < sizeof( ProtoBufMsgHeader_t ) )
{
uint32 unMsgTypeNoFlag = unMsgType & (~k_EMsgProtoBufFlag);
AssertMsg3( false, "Received packet %s(%u) from %s less than the minimum protobuf size", PchMsgNameFromEMsg( unMsgTypeNoFlag ), unMsgTypeNoFlag, senderID.Render() );
return NULL;
}
// make a new packet for the message so we can dispatch it
// The CNetPacket takes ownership of the buffer allocated above
CNetPacket *pGCPacket = CNetPacketPool::AllocNetPacket();
pGCPacket->Init( cubData );
// copy the bits for the message over to the full size buffer
Q_memcpy( pGCPacket->PubData(), pubData, cubData );
CProtoBufNetPacket *pMsgNetPacket = new CProtoBufNetPacket( pGCPacket, eReplyType, senderID, nGCDirIndex, unMsgType & ( ~k_EMsgProtoBufFlag ) );
// release the inner packet since the wrapper now has a ref to it
pGCPacket->Release();
if ( !pMsgNetPacket->IsValid() )
{
pMsgNetPacket->Release();
return NULL;
}
return pMsgNetPacket;
}
else
{
//note that we do not currently support reply to GC messages through this pipeline
AssertMsg( eReplyType != GCProtoBufMsgSrc_FromGC, "Warning: Encountered a message from GC to GC that was not of protobuff type, will be unable to reply to this message. Message type: %d", unMsgType );
if ( cubData < sizeof( GCMsgHdrEx_t ) - sizeof( GCMsgHdr_t ) )
{
AssertMsg( false, "Received packet %s(%u) from %s less than the minimum struct size", PchMsgNameFromEMsg( unMsgType ), unMsgType, senderID.Render() );
return NULL;
}
// Determine the size of the packet. sizeof(GCMsgHdr_t) was not sent as part of the data
uint32 unFullSize = cubData + sizeof( GCMsgHdr_t );
// make a new packet for the message so we can dispatch it
// The CNetPacket takes ownership of the buffer allocated above
CNetPacket *pGCPacket = CNetPacketPool::AllocNetPacket();
pGCPacket->Init( unFullSize );
//fill in our header and copy over the body
uint8 *pFullPacket = pGCPacket->PubData();
// get the header so we can fix it up
GCMsgHdrEx_t *pHdr = (GCMsgHdrEx_t *)pFullPacket;
//pHdr->m_nSrcGCDirIndex = nGCDirIndex;
pHdr->m_eMsg = unMsgType;
pHdr->m_ulSteamID = senderID.ConvertToUint64();
// copy the bits for the message over to the full size buffer
Q_memcpy( pFullPacket+sizeof(GCMsgHdr_t), pubData, cubData );
CStructNetPacket *pMsgNetPacket = new CStructNetPacket( pGCPacket );
// release the packet
pGCPacket->Release();
return pMsgNetPacket;
}
}
//-----------------------------------------------------------------------------
// Purpose: Processes an incoming message from the client by turning it into a
// CGCMsg and sending it on to a job.
//-----------------------------------------------------------------------------
void CGCBase::MessageFromClient( const CSteamID & senderID, uint32 unMsgType, void *pubData, uint32 cubData )
{
VPROF_BUDGET( "CGCBase::MessageFromClient", VPROF_BUDGETGROUP_STEAM );
// if we don't have a GCHost yet, we won't be able to do much with this message
if( !GGCHost() )
return;
if ( OnMessageFromClient( senderID, unMsgType, pubData, cubData ) )
return;
// Rate limit messages from ordinary clients
if ( senderID.IsValid() )
{
MsgType_t eMsg = unMsgType & ~k_EMsgProtoBufFlag;
if ( m_MsgRateLimit.BIsRateLimited( senderID, eMsg ) )
{
g_RateLimitTracker.TrackRateLimitedMsg( senderID, eMsg );
return;
}
}
// !FIXME! DOTAMERGE
uint32 nGCDirIndex = 0; // GetGCDirIndex()
IMsgNetPacket *pMsgNetPacket = CreateIMsgNetPacket( GCProtoBufMsgSrc_FromSteamID, senderID, nGCDirIndex, unMsgType, pubData, cubData );
if ( NULL == pMsgNetPacket )
return;
// dispatch the packet (some messages require special consideration)
switch( unMsgType )
{
case k_EGCMsgWGRequest:
m_wgJobMgr.BHandleMsg( pMsgNetPacket );
g_theMessageList.TallySendMessage( pMsgNetPacket->GetEMsg(), cubData );
break;
default:
GetJobMgr().BRouteMsgToJob( this, pMsgNetPacket, JobMsgInfo_t( pMsgNetPacket->GetEMsg(), pMsgNetPacket->GetSourceJobID(), pMsgNetPacket->GetTargetJobID(), k_EServerTypeGC ) );
g_theMessageList.TallySendMessage( pMsgNetPacket->GetEMsg(), cubData );
break;
}
// release the packet
pMsgNetPacket->Release();
}
//-----------------------------------------------------------------------------
// Purpose: Sends a message to the given SteamID
//-----------------------------------------------------------------------------
bool CGCBase::BSendGCMsgToClient( const CSteamID & steamIDTarget, const CGCMsgBase& msg )
{
g_theMessageList.TallySendMessage( msg.Hdr().m_eMsg, msg.CubPkt() - sizeof(GCMsgHdr_t) );
VPROF_BUDGET( "GCHost", VPROF_BUDGETGROUP_STEAM );
{
VPROF_BUDGET( "GCHost - SendMessageToClient", VPROF_BUDGETGROUP_STEAM );
return m_pHost->BSendMessageToClient( m_unAppID, steamIDTarget, msg.Hdr().m_eMsg, msg.PubPkt() + sizeof(GCMsgHdr_t), msg.CubPkt() - sizeof(GCMsgHdr_t) );
}
}
//-----------------------------------------------------------------------------
// Purpose: Used to send protobuf system messages to a client
//-----------------------------------------------------------------------------
class CProtoBufClientSendHandler : public CProtoBufMsgBase::IProtoBufSendHandler
{
public:
CProtoBufClientSendHandler( const CSteamID & steamIDTarget )
: m_steamIDTarget( steamIDTarget ), m_cubSent( 0 ) {}
virtual bool BAsyncSend( MsgType_t eMsg, const uint8 *pubMsgBytes, uint32 cubSize ) OVERRIDE
{
m_cubSent = cubSize;
// !FIXME! DOTAMERGE
//return GGCInterface()->BProcessSystemMessage( eMsg | k_EMsgProtoBufFlag, pubMsgBytes, cubSize );
g_theMessageList.TallySendMessage( eMsg & ~k_EMsgProtoBufFlag, cubSize );
VPROF_BUDGET( "GCHost", VPROF_BUDGETGROUP_STEAM );
{
VPROF_BUDGET( "GCHost - SendMessageToClient (ProtoBuf)", VPROF_BUDGETGROUP_STEAM );
return GGCHost()->BSendMessageToClient( GGCBase()->GetAppID(), m_steamIDTarget, eMsg | k_EMsgProtoBufFlag, pubMsgBytes, cubSize );
}
}
uint32 GetCubSent() const { return m_cubSent; }
private:
uint32 m_cubSent;
CSteamID m_steamIDTarget;
};
//-----------------------------------------------------------------------------
// Purpose: Used to send protobuf system messages into the GC
//-----------------------------------------------------------------------------
class CProtoBufSystemSendHandler : public CProtoBufMsgBase::IProtoBufSendHandler
{
public:
CProtoBufSystemSendHandler()
: m_cubSent( 0 ) {}
virtual bool BAsyncSend( MsgType_t eMsg, const uint8 *pubMsgBytes, uint32 cubSize ) OVERRIDE
{
m_cubSent = cubSize;
// !FIXME! DOTAMERGE
//return GGCInterface()->BProcessSystemMessage( eMsg | k_EMsgProtoBufFlag, pubMsgBytes, cubSize );
g_theMessageList.TallySendMessage( eMsg & ~k_EMsgProtoBufFlag, cubSize );
VPROF_BUDGET( "GCHost", VPROF_BUDGETGROUP_STEAM );
{
VPROF_BUDGET( "GCHost - SendMessageToSystem (ProtoBuf)", VPROF_BUDGETGROUP_STEAM );
return GGCHost()->BSendMessageToClient( GGCBase()->GetAppID(), CSteamID(), eMsg | k_EMsgProtoBufFlag, pubMsgBytes, cubSize );
}
}
uint32 GetCubSent() const { return m_cubSent; }
private:
uint32 m_cubSent;
};
//-----------------------------------------------------------------------------
// Purpose: Sends a message to the given SteamID
//-----------------------------------------------------------------------------
bool CGCBase::BSendGCMsgToClient( const CSteamID & steamIDTarget, const CProtoBufMsgBase& msg )
{
CProtoBufClientSendHandler sender( steamIDTarget );
return msg.BAsyncSend( sender );
}
//-----------------------------------------------------------------------------
// Purpose: Sends a system message to the GC Host
//-----------------------------------------------------------------------------
bool CGCBase::BSendSystemMessage( const CGCMsgBase& msg, uint32 *pcubSent )
{
uint32 cubSent = msg.CubPkt() - sizeof(GCMsgHdr_t);
if ( NULL != pcubSent )
{
*pcubSent = cubSent;
}
// !FIXME! DOTAMERGE
//return GGCInterface()->BProcessSystemMessage( msg.Hdr().m_eMsg, msg.PubPkt() + sizeof(GCMsgHdr_t), cubSent );
return BSendGCMsgToClient( CSteamID(), msg );
}
//-----------------------------------------------------------------------------
// Purpose: Sends a system message to the GC Host
//-----------------------------------------------------------------------------
bool CGCBase::BSendSystemMessage( const CProtoBufMsgBase & msg, uint32 *pcubSent )
{
CProtoBufSystemSendHandler sender;
bool bRet = msg.BAsyncSend( sender );
if ( NULL != pcubSent )
{
*pcubSent = sender.GetCubSent();
}
return bRet;
}
bool CGCBase::BSendSystemMessage( const ::google::protobuf::Message &msgOut, MsgType_t eSendMsg )
{
CProtoBufSystemSendHandler sender;
CMsgProtoBufHeader hdr;
return CProtoBufMsgBase::BAsyncSendProto( sender, eSendMsg, hdr, msgOut );
}
//-----------------------------------------------------------------------------
// Purpose: send msgOut to the place that msgIn came from
//-----------------------------------------------------------------------------
bool CGCBase::BReplyToMessage( CGCMsgBase &msgOut, const CGCMsgBase &msgIn )
{
// Don't reply if the source is not expecting it
if ( !msgIn.BIsExpectingReply() )
return true;
msgOut.Hdr().m_JobIDTarget = msgIn.Hdr().m_JobIDSource;
return BSendGCMsgToClient( msgIn.Hdr().m_ulSteamID, msgOut );
}
//-----------------------------------------------------------------------------
// Purpose: send msgOut to the place that msgIn came from
//-----------------------------------------------------------------------------
bool CGCBase::BReplyToMessage( CProtoBufMsgBase &msgOut, const CProtoBufMsgBase &msgIn )
{
// Don't reply if the source is not expecting it
if ( !msgIn.GetJobIDSource() )
return true;
msgOut.SetJobIDTarget( msgIn.GetJobIDSource() );
return BSendGCMsgToClient( msgIn.GetClientSteamID(), msgOut );
}
//-----------------------------------------------------------------------------
// Purpose: Sends a message to the given SteamID
//-----------------------------------------------------------------------------
bool CGCBase::BSendGCMsgToClientWithPreSerializedBody( const CSteamID & steamIDTarget, MsgType_t eMsgType, const CMsgProtoBufHeader& hdr, const byte *pubBody, uint32 cubBody ) const
{
CProtoBufClientSendHandler sender( steamIDTarget );
return CProtoBufMsgBase::BAsyncSendWithPreSerializedBody( sender, eMsgType, hdr, pubBody, cubBody );
}
//-----------------------------------------------------------------------------
// Purpose: Sends a message that has already been packed to the system handler
//-----------------------------------------------------------------------------
bool CGCBase::BSendGCMsgToSystemWithPreSerializedBody( MsgType_t eMsgType, const CMsgProtoBufHeader& hdr, const byte *pubBody, uint32 cubBody ) const
{
CProtoBufSystemSendHandler sender;
return CProtoBufMsgBase::BAsyncSendWithPreSerializedBody( sender, eMsgType, hdr, pubBody, cubBody );
}
//-----------------------------------------------------------------------------
// Purpose: send msgOut to the place that msgIn came from
//-----------------------------------------------------------------------------
bool CGCBase::BReplyToMessageWithPreSerializedBody( MsgType_t eMsgType, const CProtoBufMsgBase &msgIn, const byte *pubBody, uint32 cubBody ) const
{
// Don't reply if the source is not expecting it
if ( !msgIn.GetJobIDSource() )
return true;
if( temp_list_mismatched_replies.GetBool() && !msgIn.BIsExpectingReply() )
{
EG_MSG( g_EGMessages, "Message %s was sent to client %s which did not expect a reply\n", PchMsgNameFromEMsg( eMsgType ), msgIn.GetClientSteamID().Render() );
}
CMsgProtoBufHeader hdr;
hdr.set_job_id_target( msgIn.GetJobIDSource() );
//is this a system message or a client message we are responding to?
bool bSystemReply = ( msgIn.GetClientSteamID() == k_steamIDNil );
if( bSystemReply )
{
return BSendGCMsgToSystemWithPreSerializedBody( eMsgType, hdr, pubBody, cubBody );
}
else
{
return BSendGCMsgToClientWithPreSerializedBody( msgIn.GetClientSteamID(), eMsgType, hdr, pubBody, cubBody );
}
}
//-----------------------------------------------------------------------------
// Purpose: send msgOut to the place that msgIn came from
//-----------------------------------------------------------------------------
bool CGCBase::BYldSendMessageAndGetReply( const CSteamID &steamIDTarget, CProtoBufMsgBase &msgOut, CProtoBufMsgBase *pMsgIn, MsgType_t eMsg )
{
CJob& curJob = GJobCur();
msgOut.ExpectingReply( curJob.GetJobID() );
if ( !BSendGCMsgToClient( steamIDTarget, msgOut ) )
return false;
if( !curJob.BYieldingWaitForMsg( pMsgIn, eMsg, steamIDTarget ) )
return false;
return true;
}
//bool CGCBase::BYldSendGCMessageAndGetReply( int32 nGCDirIndex, CProtoBufMsgBase &msgOut, CProtoBufMsgBase *pMsgIn, MsgType_t eMsg )
//{
// CJob& curJob = GJobCur();
// msgOut.ExpectingReply( curJob.GetJobID() );
//
// if ( !BSendGCMessage( nGCDirIndex, msgOut ) )
// return false;
//
// if( !curJob.BYieldingWaitForMsg( pMsgIn, eMsg, CSteamID() ) )
// return false;
//
// return true;
//}
bool CGCBase::BYldSendSystemMessageAndGetReply( CGCMsgBase &msgOut, CGCMsgBase *pMsgIn, MsgType_t eMsg )
{
CJob& curJob = GJobCur();
msgOut.ExpectingReply( curJob.GetJobID() );
if ( !BSendSystemMessage( msgOut ) )
return false;
if( !curJob.BYieldingWaitForMsg( pMsgIn, eMsg, CSteamID() ) )
return false;
return true;
}
bool CGCBase::BYldSendSystemMessageAndGetReply( CProtoBufMsgBase &msgOut, CProtoBufMsgBase *pMsgIn, MsgType_t eMsg )
{
CJob& curJob = GJobCur();
msgOut.ExpectingReply( curJob.GetJobID() );
if ( !BSendSystemMessage( msgOut ) )
return false;
if( !curJob.BYieldingWaitForMsg( pMsgIn, eMsg, CSteamID() ) )
return false;
return true;
}
bool CGCBase::BYldSendSystemMessageAndGetReply( const ::google::protobuf::Message &msgSend, MsgType_t eSendMsg, ::google::protobuf::Message *pMsgResponse, MsgType_t eRespondMsg )
{
CJob& curJob = GJobCur();
CMsgProtoBufHeader hdr;
hdr.set_job_id_source( curJob.GetJobID() );
CProtoBufSystemSendHandler sender;
CProtoBufMsgBase::BAsyncSendProto( sender, eSendMsg, hdr, msgSend );
CProtoBufPtrMsg protoMsg( pMsgResponse );
//return curJob.BYieldingWaitForMsg( &protoMsg, eRespondMsg, CSteamID() );
return curJob.BYieldingWaitForMsg( &protoMsg, eRespondMsg ); // !FIXME! For some reason system replies are coming back with a universe and instance set (but account ID zero).
}
//-----------------------------------------------------------------------------
// Purpose: Creates a new session for the steam ID
//-----------------------------------------------------------------------------
CGCUserSession *CGCBase::CreateUserSession( const CSteamID & steamID, CGCSharedObjectCache *pSOCache ) const
{
return new CGCUserSession( steamID, pSOCache );
}
//-----------------------------------------------------------------------------
// Purpose: Creates a new session for the steam ID
//-----------------------------------------------------------------------------
CGCGSSession *CGCBase::CreateGSSession( const CSteamID & steamID, CGCSharedObjectCache *pSOCache, uint32 unServerAddr, uint16 usServerPort ) const
{
return new CGCGSSession( steamID, pSOCache, unServerAddr, usServerPort );
}
//-----------------------------------------------------------------------------
// Purpose: Locks the session for this steam ID and returns it. Returns NULL
// if the lock could not be granted or if the session could not be
// found.
//-----------------------------------------------------------------------------
CGCUserSession *CGCBase::YieldingGetLockedUserSession( const CSteamID & steamID, const char *pszFilename, int nLineNum )
{
if( !steamID.BIndividualAccount() )
return NULL;
if( !BYieldingLockSteamID( steamID, pszFilename, nLineNum ) )
return NULL;
CGCUserSession *pSession = FindUserSession( steamID );
if( !pSession )
{
//EmitInfo( SPEW_GC, SPEW_ALWAYS, LOG_ALWAYS, "Unable to find session %s to lock it. Attempting to fetch it from the AM\n", steamID.Render() );
pSession = (CGCUserSession *)YieldingRequestSession( steamID );
if( !pSession )
{
UnlockSteamID( steamID );
}
}
return pSession;
}
//-----------------------------------------------------------------------------
// Purpose: Checks if a user is in the start playing queue
//-----------------------------------------------------------------------------
bool CGCBase::BUserSessionPending( const CSteamID & steamID ) const
{
int nStartPlayingMapIndex = m_mapStartPlayingQueueIndexBySteamID.Find( steamID );
return ( nStartPlayingMapIndex != m_mapStartPlayingQueueIndexBySteamID.InvalidIndex() );
}
//-----------------------------------------------------------------------------
// Purpose: Returns the session for this steamID or NULL if that session could
// not be found.
//-----------------------------------------------------------------------------
CGCUserSession *CGCBase::FindUserSession( const CSteamID & steamID ) const
{
// we should only call this on individual ids
if ( !steamID.IsValid() )
{
AssertMsg1( steamID.IsValid(), "CGCBase::FindUserSession was passed invalid Steam ID %s", steamID.Render() );
return NULL;
}
if ( !steamID.BIndividualAccount() )
{
AssertMsg1( steamID.BIndividualAccount(), "CGCBase::FindUserSession was passed non-individual Steam ID %s", steamID.Render() );
return NULL;
}
CGCUserSession **ppSession = m_hashUserSessions.PvRecordFind( steamID.ConvertToUint64() );
if( ppSession )
{
(*ppSession)->MarkAccess();
return *ppSession;
}
else
{
return NULL;
}
}
//-----------------------------------------------------------------------------
// Purpose: Returns true if the session associated with the steam id is online, false otherwise
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingIsOnline( const CSteamID & steamID )
{
CGCMsg< MsgGCValidateSession_t > msg( k_EGCMsgValidateSession );
msg.Body().m_ulSteamID = steamID.ConvertToUint64();
msg.ExpectingReply( GJobCur().GetJobID() );
if( !BSendSystemMessage( msg ) )
return false;
CGCMsg< MsgGCValidateSessionResponse_t > msgReply;
if( !GJobCur().BYieldingWaitForMsg( &msgReply, k_EGCMsgValidateSessionResponse ) )
{
EmitWarning( SPEW_GC, LOG_ALWAYS, "Didn't get reply from AM for %s in YieldingRequestSession\n", steamID.Render() );
return false;
}
return msgReply.Body().m_bOnline;
}
//-----------------------------------------------------------------------------
// Purpose: Looks up a session from the AM for the provided steam ID.
//-----------------------------------------------------------------------------
template <typename T >
class CScopedIncrement
{
public:
inline CScopedIncrement( T & counter) : m_counter(counter) { ++m_counter; }
inline ~CScopedIncrement() { --m_counter; }
private:
T &m_counter;
};
CGCSession *CGCBase::YieldingRequestSession( const CSteamID & steamID )
{
AssertRunningJob();
if( !steamID.BIndividualAccount() && !steamID.BGameServerAccount() )
return NULL;
Assert( IsSteamIDUnlockedOrLockedByCurJob( steamID ) );
// Check if we already have info in the logon queue for this SteamID
int nStartPlayingMapIndex = m_mapStartPlayingQueueIndexBySteamID.Find( steamID );
if ( nStartPlayingMapIndex != m_mapStartPlayingQueueIndexBySteamID.InvalidIndex() )
{
// Sanity
int idxStartPlayingQueue = m_mapStartPlayingQueueIndexBySteamID[ nStartPlayingMapIndex ];
Assert( m_llStartPlaying[ idxStartPlayingQueue ].m_steamID == steamID );
// Pull the logon out of the queue and execute it NOW
YieldingExecuteStartPlayingQueueEntryByIndex( idxStartPlayingQueue );
// Now return the session that was created, if any
return FindUserOrGSSession( steamID );
}
CGCMsg< MsgGCValidateSession_t > msg( k_EGCMsgValidateSession );
msg.Body().m_ulSteamID = steamID.ConvertToUint64();
msg.ExpectingReply( GJobCur().GetJobID() );
if( !BSendSystemMessage( msg ) )
return NULL;
CScopedIncrement<int> increment( m_nRequestSessionJobsActive );
CGCMsg< MsgGCValidateSessionResponse_t > msgReply;
if( !GJobCur().BYieldingWaitForMsg( &msgReply, k_EGCMsgValidateSessionResponse ) )
{
EmitWarning( SPEW_GC, LOG_ALWAYS, "Didn't get reply from AM for %s in YieldingRequestSession\n", steamID.Render() );
return NULL;
}
if( steamID.BIndividualAccount() )
{
if( msgReply.Body().m_bOnline )
{
CUtlBuffer bufVarData;
if( msgReply.CubVarData() )
{
bufVarData.Put( msgReply.PubVarData(), msgReply.CubVarData() );
}
// Check if they have an entry in the startplaying queue, then get rid of it!
// They data we just received is the most up-to-date we have. We should
// prefer this data over anything in the queue for sure.
BRemoveStartPlayingQueueEntry( steamID );
YieldingStartPlaying( steamID, msgReply.Body().m_ulSteamIDGS, msgReply.Body().m_unServerAddr, msgReply.Body().m_usServerPort, msgReply.CubVarData() ? &bufVarData : NULL );
return FindUserSession( steamID );
}
else
{
//EmitWarning( SPEW_GC, LOG_ALWAYS, "Reply from AM is logging off %s in YieldingRequestSession\n", steamID.Render() );
YieldingStopPlaying( steamID );
return NULL;
}
}
else
{
if( msgReply.Body().m_bOnline )
{
YieldingStartGameserver( steamID, msgReply.Body().m_unServerAddr, msgReply.Body().m_usServerPort, msgReply.PubVarData(), msgReply.CubVarData() );
return FindGSSession( steamID );
}
else
{
//EmitWarning( SPEW_GC, LOG_ALWAYS, "Reply from AM is stopping %s in YieldingRequestSession\n", steamID.Render() );
YieldingStopGameserver( steamID );
return NULL;
}
}
}
//-----------------------------------------------------------------------------
// Purpose: Send outgoing HTTP request to some other server. Probably a WebAPI
// request to steam itself, but it could be a request on a more
// remote server.
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingSendHTTPRequest( const CHTTPRequest *pRequest, CHTTPResponse *pResponse )
{
if ( !pRequest || !pResponse )
{
AssertMsg( false, "Bad parameters for BYieldingSendHTTPRequest" );
return false;
}
CMsgHttpResponse msgResponse;
if( !BYldSendSystemMessageAndGetReply( pRequest->GetProtoObj(), k_EGCMsgSendHTTPRequest, &msgResponse, k_EGCMsgSendHTTPRequestResponse ) )
{
ReportHTTPError( CFmtStr( "No response to HTTP system message for %s", pRequest->GetURL() ), CGCEmitGroup::kMsg_Error );
return false;
}
if ( !msgResponse.has_status_code() )
{
ReportHTTPError( CFmtStr( "No status code on HTTP response for %s", pRequest->GetURL() ), CGCEmitGroup::kMsg_Error );
return false;
}
//log the result of this request
if( msgResponse.status_code() != k_EHTTPStatusCode200OK )
{
ReportHTTPError( CFmtStr( "Invalid status code %u for %s", msgResponse.status_code(), pRequest->GetURL() ), CGCEmitGroup::kMsg_Warning );
}
else
{
ReportHTTPError( CFmtStr( "Success status code for %s", pRequest->GetURL() ), CGCEmitGroup::kMsg_Verbose );
}
pResponse->DeserializeFromProtoBuf( msgResponse );
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Send an outgoing HTTP request and parse the result into KeyValues.
//-----------------------------------------------------------------------------
EResult CGCBase::YieldingSendHTTPRequestKV( const CHTTPRequest *pRequest, KeyValues *pKVResponse )
{
CHTTPResponse apiResponse;
if ( !BYieldingSendHTTPRequest( pRequest, &apiResponse ) )
{
EmitError( SPEW_GC, __FUNCTION__ ": web call to %s timed out\n", pRequest->GetURL() );
return k_EResultTimeout;
}
if ( k_EHTTPStatusCode200OK != apiResponse.GetStatusCode() )
{
EmitError( SPEW_GC, __FUNCTION__ ": web call to %s got failure code %d\n", pRequest->GetURL(), apiResponse.GetStatusCode() );
return k_EResultRemoteCallFailed;
}
pKVResponse->UsesEscapeSequences( true );
if ( !pKVResponse->LoadFromBuffer( "webResponse", *apiResponse.GetBodyBuffer() ) )
{
EmitError( SPEW_GC, "Web call to %s could not parse response\n", pRequest->GetURL() );
return k_EResultRemoteCallFailed;
}
return k_EResultOK;
}
//-----------------------------------------------------------------------------
// Purpose: Locks the session for this steam ID and returns it. Returns NULL
// if the lock could not be granted or if the session could not be
// found.
//-----------------------------------------------------------------------------
CGCGSSession *CGCBase::YieldingGetLockedGSSession( const CSteamID & steamID, const char *pszFilename, int nLineNum )
{
if( !steamID.BGameServerAccount() )
return NULL;
if( !BYieldingLockSteamID( steamID, pszFilename, nLineNum ) )
return NULL;
CGCGSSession *pSession = FindGSSession( steamID );
if( !pSession )
{
pSession = (CGCGSSession *)YieldingRequestSession( steamID );
if( !pSession )
{
UnlockSteamID( steamID );
}
}
return pSession;
}
void CGCBase::ReportHTTPError( const char* pszError, CGCEmitGroup::EMsgLevel eLevel )
{
//see if we can find a match
int nIndex = m_HTTPErrors.Find( pszError );
if( nIndex != m_HTTPErrors.InvalidIndex() )
{
//just increment our count
m_HTTPErrors[ nIndex ]->m_nCount++;
m_HTTPErrors[ nIndex ]->m_eSeverity = MIN( eLevel, m_HTTPErrors[ nIndex ]->m_eSeverity );
}
else
{
//add one
SHTTPError* pError = new SHTTPError;
pError->m_sStr = pszError;
pError->m_nCount = 1;
pError->m_eSeverity = eLevel;
m_HTTPErrors.Insert( pError->m_sStr, pError );
}
if( !m_DumpHTTPErrorsSchedule.BIsScheduled() )
{
m_DumpHTTPErrorsSchedule.ScheduleMS( this, &CGCBase::DumpHTTPErrors, 1000 );
}
}
void CGCBase::DumpHTTPErrors()
{
FOR_EACH_MAP_FAST( m_HTTPErrors, nCurrError )
{
SHTTPError* pError = m_HTTPErrors[ nCurrError ];
EG_EMIT( g_EGHTTPRequest, m_HTTPErrors[ nCurrError ]->m_eSeverity, "%s - %d times\n", pError->m_sStr.String(), pError->m_nCount );
delete pError;
}
m_HTTPErrors.RemoveAll();
}
//-----------------------------------------------------------------------------
// Purpose: Returns the session for this steamID or NULL if that session could
// not be found.
//-----------------------------------------------------------------------------
CGCGSSession *CGCBase::YieldingFindOrCreateGSSession( const CSteamID & steamID, uint32 unServerAddr, uint16 usServerPort, const uint8 *pubVarData, uint32 cubVarData )
{
Assert( IsSteamIDLockedByJob( steamID, &GJobCur() ) );
// If it's not a game server ID, then we shouldn't make a session for it.
if( !steamID.BGameServerAccount() )
return NULL;
MEM_ALLOC_CREDIT_( "YieldingFindOrCreateGSSession" );
// if var data came with this StartPlaying message, parse it into a KV and stick it on the session
KeyValues *pkvDetails = NULL;
if( pubVarData && cubVarData )
{
CUtlBuffer bufDetails;
bufDetails.Put( pubVarData, cubVarData );
pkvDetails = new KeyValues( "SessionDetails" );
if( !pkvDetails->ReadAsBinary( bufDetails ) )
{
EmitError( SPEW_GC, "Unable to parse session details for %s\n", steamID.Render() );
pkvDetails->deleteThis();
pkvDetails = NULL;
}
}
// // Since we might have to lock the session in some cases, let's just always grab the lock here,
// // to keep things simpler.
// if ( !BYieldingLockSteamID( steamID, __FILE__, __LINE__ ) )
// return NULL;
CGCGSSession *pSession = FindGSSession( steamID );
CGCSharedObjectCache *pSOCache = NULL;
if( !pSession )
{
pSOCache = YieldingFindOrLoadSOCache( steamID );
// Did anybody create a session while we held the lock?
// We hold the lock, and you must hold the lock to create
// the session, so this race condition should be impossible
pSession = FindGSSession( steamID );
Assert( pSession == NULL );
}
if( !pSession )
{
// Create session of app-specific type
pSession = CreateGSSession( steamID, pSOCache, unServerAddr, usServerPort );
Assert( pSession );
if ( !pSession )
{
AssertMsg1( false, "Failed creating GC GS session for %llu", steamID.ConvertToUint64() );
if ( pkvDetails )
{
pkvDetails->deleteThis();
}
//UnlockSteamID( steamID ); // I like to clean up after myself
return NULL;
}
RemoveCacheFromLRU( pSOCache );
CGCGSSession **ppSession = m_hashGSSessions.PvRecordInsert( steamID.ConvertToUint64() );
*ppSession = pSession;
// Do game-specific work
YieldingSessionStartServer( pSession );
}
else
{
if ( unServerAddr != 0 && usServerPort != 0 && ( unServerAddr != pSession->GetAddr() || usServerPort != pSession->GetPort() ) )
{
UpdateGSSessionAddress( pSession, unServerAddr, usServerPort );
}
}
if( pkvDetails )
{
uint32 ip = pkvDetails->GetInt( "ip", 0 );
if ( ip != 0 )
pSession->m_unIPPublic = ip;
pSession->m_osType = static_cast<EOSType>( pkvDetails->GetInt( "osType", k_eOSUnknown ) );
pSession->m_bIsTestSession = pkvDetails->GetInt( "isTestSession", 0 ) != 0;
pkvDetails->deleteThis();
}
//UnlockSteamID( steamID ); // I like to clean up after myself
return pSession;
}
//-----------------------------------------------------------------------------
// Purpose: Called when a Session is moved to a different address.
//-----------------------------------------------------------------------------
void CGCBase::UpdateGSSessionAddress( CGCGSSession *pSession, uint32 unServerAddr, uint16 usServerPort )
{
pSession->SetIPAndPort( unServerAddr, usServerPort );
}
//-----------------------------------------------------------------------------
// Purpose: Returns the session for this steamID or NULL if that session could
// not be found.
//-----------------------------------------------------------------------------
CGCGSSession *CGCBase::FindGSSession( const CSteamID & steamID ) const
{
// we should only call this on server ids
if ( !steamID.IsValid() || steamID.GetAccountID() == 0 )
{
AssertMsg1( false, "CGCBase::FindGSSession was passed invalid Steam ID %s", steamID.Render() );
return NULL;
}
if ( !steamID.BGameServerAccount() )
{
AssertMsg1( steamID.BGameServerAccount(), "CGCBase::FindGSSession was passed non-gameserver Steam ID %s", steamID.Render() );
return NULL;
}
CGCGSSession **ppSession = m_hashGSSessions.PvRecordFind( steamID.ConvertToUint64() );
if( ppSession )
{
(*ppSession)->MarkAccess();
return *ppSession;
}
else
{
return NULL;
}
}
//-----------------------------------------------------------------------------
// Purpose: Locate session from appropriate table, depending on if it's
// an individual or gameserver ID
//-----------------------------------------------------------------------------
CGCSession *CGCBase::FindUserOrGSSession( const CSteamID & steamID ) const
{
if ( steamID.BIndividualAccount() )
return FindUserSession( steamID );
if ( steamID.BGameServerAccount() )
return FindGSSession( steamID );
AssertMsg1( false, "CGCBase::FindUserOrGSSession, steam ID %s isn't an individual or a gameserver ID", steamID.Render() );
return NULL;
}
//-----------------------------------------------------------------------------
// Purpose: Wakes up the job waiting for this SQL result
//-----------------------------------------------------------------------------
void CGCBase::SQLResults( GID_t gidContextID )
{
VPROF_BUDGET( "CGCBase::SQLResults", VPROF_BUDGETGROUP_STEAM );
m_JobMgr.BResumeSQLJob( gidContextID );
}
//-----------------------------------------------------------------------------
// Purpose: Finds the cache in the map for a new session
//-----------------------------------------------------------------------------
CGCSharedObjectCache *CGCBase::FindSOCache( const CSteamID & steamID )
{
CUtlMap< CSteamID, CGCSharedObjectCache *, int >::IndexType_t nCache = m_mapSOCache.Find( steamID );
if( m_mapSOCache.IsValidIndex( nCache ) )
return m_mapSOCache[nCache];
else
return NULL;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingLoadSOCache( CGCSharedObjectCache *pSOCache )
{
return true;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CGCBase::YieldingSOCacheLoaded( CGCSharedObjectCache *pSOCache )
{
// remove it, so we don't stomp the copy in memcached
m_rbtreeSOCachesWithDirtyVersions.Remove( pSOCache->GetOwner() );
// stomp the version with the one we set in memcached previously if possible, otherwise, re-add it to the set
if ( !BYieldingRetrieveCacheVersion( pSOCache ) )
{
m_rbtreeSOCachesWithDirtyVersions.InsertIfNotFound( pSOCache->GetOwner() );
}
}
//-----------------------------------------------------------------------------
// Purpose: Removes the cache for this steamID
//-----------------------------------------------------------------------------
void CGCBase::RemoveSOCache( const CSteamID & steamID )
{
CUtlMap< CSteamID, CGCSharedObjectCache *, int >::IndexType_t nCache = m_mapSOCache.Find( steamID );
if( m_mapSOCache.IsValidIndex( nCache ) )
{
CGCSharedObjectCache *pSOCache = m_mapSOCache[nCache];
pSOCache->RemoveAllSubscribers();
if( pSOCache->BIsDatabaseDirty() )
{
EmitError( SPEW_GC, "Attempting to remove SO Cache %s while it was dirty. Adding to Writeback instead\n", steamID.Render() );
pSOCache->DumpDirtyObjects();
AddCacheToWritebackQueue( pSOCache );
// adding the cache to the LRU list too, just so it will go away once writeback does its thing
if( !m_listCachesToUnload.IsValidIndex( pSOCache->GetLRUHandle() ) )
{
AddCacheToLRU( pSOCache );
}
}
else
{
RemoveCacheFromLRU(pSOCache);
delete pSOCache;
m_mapSOCache.RemoveAt( nCache );
}
}
}
//-----------------------------------------------------------------------------
// Purpose: Enqueues a flush instruction to Econ service for Web Inventory to update
//-----------------------------------------------------------------------------
void CGCBase::FlushInventoryCache( AccountID_t unAccountID )
{
VPROF_BUDGET( "FlushInventoryCache - enqueue", VPROF_BUDGETGROUP_STEAM );
m_rbFlushInventoryCacheAccounts.InsertIfNotFound( unAccountID );
}
//-----------------------------------------------------------------------------
// Purpose: Finds the cache in the map for a new session and locks it
//-----------------------------------------------------------------------------
bool CGCBase::UnloadUnusedCaches( uint32 unMaxCacheCount, CLimitTimer *pLimitTimer )
{
VPROF_BUDGET( "UnloadUnusedCaches", VPROF_BUDGETGROUP_STEAM );
uint32 unCachesUnloaded = 0;
for( uint32 unCache = m_listCachesToUnload.Head(), unNextCache = m_listCachesToUnload.InvalidIndex(); unCache != m_listCachesToUnload.InvalidIndex(); unCache = unNextCache )
{
unNextCache = m_listCachesToUnload.Next( unCache );
// only remove caches until we are under our limit
if( (uint32)m_mapSOCache.Count() <= unMaxCacheCount )
return false;
// only loop until we need to stop consuming heartbeat time. We'll finish in later frames
if( pLimitTimer && pLimitTimer->BLimitReached() )
return true;
CSteamID ownerID = m_listCachesToUnload[ unCache ];
CGCSharedObjectCache *pSOCache = FindSOCache( ownerID );
Assert( pSOCache );
if( !pSOCache )
{
EmitError( SPEW_GC, "Cache for %s could not be found even though it is in the LRU list\n", ownerID.Render() );
m_listCachesToUnload.Remove( unCache );
continue;
}
// make sure there's no session using this cache
if( ( ownerID.BIndividualAccount() && FindUserSession( ownerID ) )
|| ( ownerID.BGameServerAccount() && FindGSSession( ownerID ) ) )
{
EmitError( SPEW_GC, "Cache for %s has a session even though it is in the LRU list\n", ownerID.Render() );
Assert( pSOCache->GetLRUHandle() == unCache );
if ( pSOCache->GetLRUHandle() != unCache )
{
EmitError( SPEW_GC, "Cache for %s has a different LRU handle than the one retrieved from the iterator! 0x%08x vs 0x%08x\n", ownerID.Render(), pSOCache->GetLRUHandle(), unCache );
}
RemoveCacheFromLRU( pSOCache );
continue;
}
// Locked steam IDs mean someone is using the cache.
// Being in the writeback queue means that you haven't actually been unused for very long.
// Just move on to the next one in those cases.
if( IsSteamIDLocked( ownerID ) || pSOCache->GetInWriteback() )
continue;
// either count down by one or still in LRU?
int iPreRemoveCount = m_listCachesToUnload.Count();
// remove and delete the cache (which will remove it from the LRU list too.)
RemoveSOCache( ownerID );
unCachesUnloaded++;
if ( iPreRemoveCount != m_listCachesToUnload.Count() + 1 &&
iPreRemoveCount != m_listCachesToUnload.Count() )
{
EmitError( SPEW_GC, "CGCBase::UnloadUnusedCaches() sanity check failed! List size changed dramatically removing 0x%08x; delta %i\n", unCache, iPreRemoveCount - m_listCachesToUnload.Count() );
}
}
return false;
}
//-----------------------------------------------------------------------------
// Purpose: Does some sanity checks on the SO cache LRU
//-----------------------------------------------------------------------------
void CGCBase::VerifySOCacheLRU()
{
CUtlRBTree<CSteamID, int> rbTreeUsersEncountered( 0, m_listCachesToUnload.Count(), DefLessFunc( CSteamID ) );
for( uint32 unCache = m_listCachesToUnload.Head(), unNextCache = m_listCachesToUnload.InvalidIndex(); unCache != m_listCachesToUnload.InvalidIndex(); unCache = unNextCache )
{
unNextCache = m_listCachesToUnload.Next( unCache );
CSteamID ownerID = m_listCachesToUnload[ unCache ];
CGCSharedObjectCache *pSOCache = FindSOCache( ownerID );
if ( !pSOCache )
{
EmitError( SPEW_GC, "CGCBase::UnloadUnusedCaches() sanity[0] check failed! Empty cache in list in slot 0x%08x\n", unCache );
continue;
}
if ( pSOCache->GetLRUHandle() != unCache )
{
EmitError( SPEW_GC, "CGCBase::UnloadUnusedCaches() sanity[1] check failed! Cache entry mismatch [ 0x%08x vs 0x%08x ] (owner: %s)\n", pSOCache->GetLRUHandle(), unCache, pSOCache->GetOwner().Render() );
}
if ( !rbTreeUsersEncountered.IsValidIndex( rbTreeUsersEncountered.InsertIfNotFound( ownerID ) ) )
{
EmitError( SPEW_GC, "CGCBase::UnloadUnusedCaches() sanity[2] check failed! Duplicate entry in list for 0x%08x (owner: %s)\n", unCache, pSOCache->GetOwner().Render() );
}
}
}
//-----------------------------------------------------------------------------
// Purpose: Adds the cache to the LRU list
//-----------------------------------------------------------------------------
void CGCBase::AddCacheToLRU( CGCSharedObjectCache * pSOCache )
{
Assert( pSOCache->GetLRUHandle() == m_listCachesToUnload.InvalidIndex() );
#if WITH_SOCACHE_LRU_DEBUGGING
if ( pSOCache->GetLRUHandle() != m_listCachesToUnload.InvalidIndex() )
{
EmitError( SPEW_GC, "CGCBase::AddCacheToLRU() sanity[4] check failed! Adding SO Cache with existing LRU handle: 0x%08x\n", pSOCache->GetLRUHandle() );
}
#endif
// remove it just in case. Crashes are bad.
RemoveCacheFromLRU( pSOCache );
#if WITH_SOCACHE_LRU_DEBUGGING
Assert( pSOCache->GetLRUHandle() == m_listCachesToUnload.InvalidIndex() );
if ( pSOCache->GetLRUHandle() != m_listCachesToUnload.InvalidIndex() )
{
EmitError( SPEW_GC, "CGCBase::AddCacheToLRU() sanity[5] check failed! Adding SO Cache with existing LRU handle: 0x%08x\n", pSOCache->GetLRUHandle() );
}
#endif
pSOCache->SetLRUHandle( m_listCachesToUnload.AddToTail( pSOCache->GetOwner() ) );
}
//-----------------------------------------------------------------------------
// Purpose: Removes the cache from the LRU list
//-----------------------------------------------------------------------------
void CGCBase::RemoveCacheFromLRU( CGCSharedObjectCache * pSOCache )
{
#if WITH_SOCACHE_LRU_DEBUGGING
if ( m_listCachesToUnload.IsValidIndex( pSOCache->GetLRUHandle() ) == ( pSOCache->GetLRUHandle() == m_listCachesToUnload.InvalidIndex() ) )
{
EmitError( SPEW_GC, "CGCBase::RemoveCacheFromLRU() sanity[6] check failed! SO Cache has an invalid index, but IsValidIndex() is returning true: 0x%08x\n", pSOCache->GetLRUHandle() );
}
#endif
if( m_listCachesToUnload.IsValidIndex( pSOCache->GetLRUHandle() ) )
{
if( m_listCachesToUnload[ pSOCache->GetLRUHandle() ] != pSOCache->GetOwner() )
{
EmitError( SPEW_GC, "CGCBase::RemoveCacheFromLRU() Attempting to remove SOCache LRU index %d for %s, which really holds %s\n",
pSOCache->GetLRUHandle(), pSOCache->GetOwner().Render(), m_listCachesToUnload[ pSOCache->GetLRUHandle() ].Render() );
}
else
{
m_listCachesToUnload.Remove( pSOCache->GetLRUHandle() );
}
}
pSOCache->SetLRUHandle( m_listCachesToUnload.InvalidIndex() );
}
//-----------------------------------------------------------------------------
// Purpose: Finds the cache in the map for a new session and locks it
//-----------------------------------------------------------------------------
CGCSharedObjectCache *CGCBase::YieldingGetLockedSOCache( const CSteamID &steamID, const char *pszFilename, int nLineNum )
{
if( !BYieldingLockSteamID( steamID, pszFilename, nLineNum ) )
return NULL;
return YieldingFindOrLoadSOCache( steamID );
}
//-----------------------------------------------------------------------------
// Purpose: Finds the cache in the map for a new session
//-----------------------------------------------------------------------------
CGCSharedObjectCache *CGCBase::YieldingFindOrLoadSOCache( const CSteamID &steamID )
{
AssertRunningJob();
if( !steamID.IsValid() )
{
AssertMsg1( false, "Unable to load SO cache for invalid steam ID %s", steamID.Render() );
EmitError( SPEW_GC, "Unable to load SO cache for invalid steam ID %s (instance: %d)\n", steamID.Render(), steamID.GetUnAccountInstance() );
return NULL;
}
// check to see if the SO cache is being loaded--if so, then we yield until it is done
// the reason we are not just locking the steam id is because the current job may have
// a lock on something else, and jobs can only have one lock active at a time.
CJobTime timeStartedWaiting;
timeStartedWaiting.SetToJobTime();
while ( m_rbtreeSOCachesBeingLoaded.Find( steamID ) != m_rbtreeSOCachesBeingLoaded.InvalidIndex() )
{
// !TEST! Looks like we might have a bug where we're spinning here waiting forever.
// Add a timeout just in case.
if ( timeStartedWaiting.CServerMicroSecsPassed() > 180 * k_nMillion )
{
AssertMsg1( false, "Timed out waiting for SO cache %s to finish loading", steamID.Render() );
return false;
}
GJobCur().BYieldingWaitOneFrame();
}
CGCSharedObjectCache *pSOCache = FindSOCache( steamID );
if( !pSOCache )
{
m_rbtreeSOCachesBeingLoaded.Insert( steamID );
pSOCache = CreateSOCache( steamID );
CJobTime timeStartedLoading;
timeStartedLoading.SetToJobTime();
if( BYieldingLoadSOCache( pSOCache ) )
{
if ( FindSOCache( steamID ) != NULL )
{
EmitError( SPEW_GC, "HOLY FUCKING SHIT WE ARE DUPLICATING SO CACHES [%s]\n", steamID.Render() );
}
m_mapSOCache.Insert( steamID, pSOCache );
float flSecondsToLoad = (float)timeStartedLoading.CServerMicroSecsPassed() / (float)k_nMillion;
if ( flSecondsToLoad > 10.0f )
{
EmitInfo( SPEW_GC, 4, 1, "Loading of SO cache for %s took %.1fs\n", steamID.Render(), flSecondsToLoad );
}
//mark this cache as loaded so that it's version can change again
pSOCache->SetDetectVersionChanges( false );
CJobTime timeStartedNotify;
timeStartedNotify.SetToJobTime();
YieldingSOCacheLoaded( pSOCache );
float flSecondsToNotify = (float)timeStartedNotify.CServerMicroSecsPassed() / (float)k_nMillion;
if ( flSecondsToNotify > 10.0f )
{
EmitInfo( SPEW_GC, 1, 1, "YieldingSOCacheLoaded for %s took %.1fs\n", steamID.Render(), flSecondsToNotify );
}
AddCacheToLRU( pSOCache ); // in case the cache isn't about to be attached to a session
m_rbtreeSOCachesBeingLoaded.Remove( steamID );
}
else
{
AssertMsg1( false, "Unable to load SO cache for %llu", steamID.ConvertToUint64() );
EmitError( SPEW_GC, "Unable to load SO cache for %llu\n", steamID.ConvertToUint64() );
delete pSOCache;
m_rbtreeSOCachesBeingLoaded.Remove( steamID );
return NULL;
}
}
else
{
// if the cache is in the LRU, move it to the end of the list
if( m_listCachesToUnload.IsValidIndex( pSOCache->GetLRUHandle() ) )
{
RemoveCacheFromLRU( pSOCache );
AddCacheToLRU( pSOCache );
}
}
return pSOCache;
}
//-----------------------------------------------------------------------------
// Purpose: Reloads the SO cache
//-----------------------------------------------------------------------------
void CGCBase::YieldingReloadCache( CGCSharedObjectCache *pSOCache )
{
Assert( IsSteamIDLockedByJob( pSOCache->GetOwner(), &GJobCur() ) );
if( !IsSteamIDLockedByJob( pSOCache->GetOwner(), &GJobCur() ) )
return;
// Flush all pending writes
CSQLAccess sqlAccess;
sqlAccess.BBeginTransaction( "CGCBase::YieldingReloadCache - Flush writes" );
pSOCache->YieldingStageAllWrites( sqlAccess );
if ( !sqlAccess.BCommitTransaction( true ) )
{
EmitError( SPEW_SHAREDOBJ, "%s: Unable to flush pending writes for %s, reload failed",
__FUNCTION__, pSOCache->GetOwner().Render() );
return;
}
// load the data into a new cache
CGCSharedObjectCache *pNewCache = CreateSOCache( pSOCache->GetOwner() );
if( !BYieldingLoadSOCache( pNewCache ) )
{
EmitError( SPEW_SHAREDOBJ, "Unable to reload cache for %s because of a SQL error", pSOCache->GetOwner().Render() );
return;
}
// process every object in the new cache and move it to the old one if necessary
FOR_EACH_MAP_FAST( CSharedObject::GetFactories(), nType )
{
int nTypeID = CSharedObject::GetFactories().Key( nType );
// remove all the old items of this type
CSharedObjectTypeCache *pOldTypeCache = pSOCache->FindTypeCache( nTypeID );
if( pOldTypeCache )
{
for( uint32 nCurrObj = 0; nCurrObj < pOldTypeCache->GetCount(); )
{
//not all objects should be deleted (for example lobbies/parties), so for those objects
//don't delete and instead just skip over them
if( pOldTypeCache->GetObject( nCurrObj )->BShouldDeleteByCache() )
{
pSOCache->RemoveObject( *pOldTypeCache->GetObject( nCurrObj ) );
}
else
{
nCurrObj++;
}
}
}
// add all the new objects of this type
CSharedObjectTypeCache *pNewTypeCache = pNewCache->FindTypeCache( nTypeID );
if( pNewTypeCache )
{
for( uint unObject = 0; unObject < pNewTypeCache->GetCount(); unObject++ )
{
pSOCache->AddObject( pNewTypeCache->GetObject( unObject ) );
}
}
}
// remove all the objects in the new cache
pNewCache->RemoveAllObjectsWithoutDeleting();
delete pNewCache;
// if there's a session for this cache, tell it about the reload
if( pSOCache->GetOwner().BIndividualAccount() )
{
CGCUserSession *pUserSession = FindUserSession( pSOCache->GetOwner() );
if( pUserSession )
pUserSession->YieldingSOCacheReloaded();
}
else if( pSOCache->GetOwner().BGameServerAccount() )
{
CGCGSSession *pGSSession = FindGSSession( pSOCache->GetOwner() );
if( pGSSession )
pGSSession->YieldingSOCacheReloaded();
}
}
//-----------------------------------------------------------------------------
// Purpose: Factory method to create a CGCSharedObjectCache
// Input : &steamID - steamID that will own the CGCSharedObjectCache
// Output : Returns a new CGCSharedObjectCache
//-----------------------------------------------------------------------------
CGCSharedObjectCache *CGCBase::CreateSOCache( const CSteamID &steamID )
{
return new CGCSharedObjectCache( steamID );
}
//-----------------------------------------------------------------------------
// Purpose: yields until the lock on the specified steamID is taken
// Input : &steamID - steamID to lock
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingLockSteamID( const CSteamID &steamID, const char *pszFilename, int nLineNum )
{
AssertRunningJob();
Assert( steamID.GetEAccountType() != k_EAccountTypePending );
// lookup
CLock *pLock = m_hashSteamIDLocks.PvRecordFind( steamID );
if ( !pLock )
{
// no lock yet, insert one
pLock = m_hashSteamIDLocks.PvRecordInsert( steamID );
pLock->SetName( steamID );
pLock->SetLockSubType( steamID.GetAccountID() );
if ( steamID.BIndividualAccount() )
{
pLock->SetLockType( k_nLockTypeIndividual );
}
else if ( steamID.BGameServerAccount() )
{
pLock->SetLockType( k_nLockTypeGameServer );
}
else
{
AssertMsg1( false, "Lock taken for unexpected steamID: %s", steamID.Render() );
}
}
Assert( pLock );
if ( !pLock )
{
EmitInfo( SPEW_GC, SPEW_ALWAYS, LOG_ALWAYS, "Unable to create lock for %s\n", steamID.Render() );
return false;
}
return GJobCur()._BYieldingAcquireLock( pLock, pszFilename, nLineNum );
}
//-----------------------------------------------------------------------------
// Purpose: locks a pair of steam IDs, grabbing the highest account ID first
// to satisfy the deadlock-avoidance code in the job system
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingLockSteamIDPair( const CSteamID &steamIDA, const CSteamID &steamIDB, const char *pszFilename, int nLineNum )
{
if( steamIDA == steamIDB )
return BYieldingLockSteamID( steamIDA, pszFilename, nLineNum );
//
// !FIXME! This is really not the correct sort criteron to use. The correct
// criteria is to use the full lock priority. For example,
// what if we pass a gameserver ID and a user ID. The whole
// concept of locking two SteamID's is probably broken when we split up
// things on the GC, though, so this might not be worth fixing.
//
if( steamIDA.GetAccountID() < steamIDB.GetAccountID() )
{
if( !BYieldingLockSteamID( steamIDB, pszFilename, nLineNum ) )
return false;
if( !BYieldingLockSteamID( steamIDA, pszFilename, nLineNum ) )
{
UnlockSteamID( steamIDB );
return false;
}
}
else
{
if( !BYieldingLockSteamID( steamIDA, pszFilename, nLineNum ) )
return false;
if( !BYieldingLockSteamID( steamIDB, pszFilename, nLineNum ) )
{
UnlockSteamID( steamIDA );
return false;
}
}
return true;
}
//-----------------------------------------------------------------------------
// Purpose: locks the specified steamID
// Input : &steamID - steamID to unlock
//-----------------------------------------------------------------------------
bool CGCBase::BLockSteamIDImmediate( const CSteamID &steamID )
{
AssertRunningJob();
Assert( steamID.GetEAccountType() != k_EAccountTypePending );
// lookup
CLock *pLock = m_hashSteamIDLocks.PvRecordFind( steamID );
if ( pLock == NULL )
{
// no lock yet, insert one
pLock = m_hashSteamIDLocks.PvRecordInsert( steamID );
Assert( pLock != NULL );
if ( pLock == NULL )
{
return false;
}
if ( steamID.BIndividualAccount() )
{
pLock->SetLockType( k_nLockTypeIndividual );
}
else if ( steamID.BGameServerAccount() )
{
pLock->SetLockType( k_nLockTypeGameServer );
}
else
{
AssertMsg1( false, "Lock taken for unexpected steamID: %s", steamID.Render() );
}
pLock->SetName( steamID );
pLock->SetLockSubType( steamID.GetAccountID() );
}
return GJobCur().BAcquireLockImmediate( pLock );
}
//-----------------------------------------------------------------------------
// Purpose: unlocks the specified steamID
// Input : &steamID - steamID to unlock
//-----------------------------------------------------------------------------
void CGCBase::UnlockSteamID( const CSteamID &steamID )
{
AssertRunningJob();
Assert( steamID.GetEAccountType() != k_EAccountTypePending );
// lookup
CLock *pLock = m_hashSteamIDLocks.PvRecordFind( steamID );
Assert( pLock );
if ( !pLock )
{
AssertMsg2( false, "UnlockSteamID( '%s' ) called by %s but unable to find lock in map", steamID.Render(), GJobCur().GetName() );
return;
}
if ( pLock->GetJobLocking() != &GJobCur() )
{
AssertMsg2( false, "UnlockSteamID( '%s' ) called when job %s doesn't own the lock", steamID.Render(), GJobCur().GetName() );
return;
}
GJobCur().ReleaseLock( pLock );
}
//-----------------------------------------------------------------------------
// Purpose: returns true if the specified steamID is locked
//-----------------------------------------------------------------------------
bool CGCBase::IsSteamIDLocked( const CSteamID &steamID )
{
CLock *pLock = m_hashSteamIDLocks.PvRecordFind( steamID );
if ( pLock )
return pLock->BIsLocked();
return false;
}
//-----------------------------------------------------------------------------
// Purpose: returns true if the specified steamID is locked by the specified job
//-----------------------------------------------------------------------------
bool CGCBase::IsSteamIDLockedByJob( const CSteamID &steamID, const CJob *pJob ) const
{
CLock *pLock = m_hashSteamIDLocks.PvRecordFind( steamID );
if ( pLock )
return ( pLock->GetJobLocking() == pJob );
return false;
}
//-----------------------------------------------------------------------------
// Purpose: returns true if the specified steamID is locked by the current job
//-----------------------------------------------------------------------------
bool CGCBase::IsSteamIDLockedByCurJob( const CSteamID &steamID ) const
{
AssertRunningJob();
return IsSteamIDLockedByJob( steamID, &GJobCur() );
}
//-----------------------------------------------------------------------------
// Purpose: returns true if the specified steamID is unlocked, or locked by the current job
//-----------------------------------------------------------------------------
bool CGCBase::IsSteamIDUnlockedOrLockedByCurJob( const CSteamID &steamID )
{
AssertRunningJob();
// lookup
CLock *pLock = m_hashSteamIDLocks.PvRecordFind( steamID );
if ( !pLock )
{
// Unlocked
return true;
}
// It is in the hash of locks and is locked return true only if it is locked by the current job
if ( pLock->BIsLocked() )
{
return ( pLock->GetJobLocking() == &GJobCur() );
}
else
{
return true;
}
}
//-----------------------------------------------------------------------------
// Purpose: returns a pointer to the lock for the steamID, or NULL if none
//-----------------------------------------------------------------------------
const CLock *CGCBase::FindSteamIDLock( const CSteamID &steamID )
{
// lookup
return m_hashSteamIDLocks.PvRecordFind( steamID );
}
//-----------------------------------------------------------------------------
// Purpose: returns a pointer to the job holding the lock for this steamID, NULL if none
//-----------------------------------------------------------------------------
CJob *CGCBase::PJobHoldingLock( const CSteamID &steamID )
{
AssertRunningJob();
// lookup
CLock *pLock = m_hashSteamIDLocks.PvRecordFind( steamID );
if ( !pLock || !pLock->BIsLocked() )
{
// Unlocked
return NULL;
}
// Return the job holding the lock
return pLock->GetJobLocking();
}
//-----------------------------------------------------------------------------
// Purpose: returns a pointer to the job holding the lock for this steamID, NULL if none
//-----------------------------------------------------------------------------
bool CGCBase::YieldingWritebackDirtyCaches( uint32 unSecondToDelayWrite )
{
CSQLAccess sqlAccess;
CUtlVector< CGCSharedObjectCache * > vecCachesWritten;
uint32 unWrittenCount = 0;
sqlAccess.BBeginTransaction( "CGCBase::YieldingWritebackDirtyCaches()" );
RTime32 unFirstTimeToWrite = time( NULL ) - unSecondToDelayWrite;
FOR_EACH_VEC( m_vecCacheWritebacks, nCache )
{
CGCSharedObjectCache *pSOCache = m_vecCacheWritebacks[ nCache ];
// if this cache entered the writeback list too frequently, skip it for now
if( unSecondToDelayWrite > 0 && pSOCache->GetWritebackTime() > unFirstTimeToWrite )
{
continue;
}
// if we can't get the lock for ourselves, catch it on the next time around
if( !BLockSteamIDImmediate( pSOCache->GetOwner() ) )
{
continue;
}
unWrittenCount += pSOCache->YieldingStageAllWrites( sqlAccess );
vecCachesWritten.AddToTail( pSOCache );
m_vecCacheWritebacks.Remove( nCache );
nCache--;
// don't hog all the CPU. Yield and wait for the next frame if
// we've been running for too long. Go ahead and write these
// caches so we don't hold their locks forever though.
if( GJobCur().GetMicrosecondsRun() > (uint64)(writeback_queue_max_accumulate_time.GetInt() * k_nThousand) ||
( writeback_queue_max_caches.GetInt() > 0 && vecCachesWritten.Count() > writeback_queue_max_caches.GetInt() ) )
{
// We've spent enough time accumulating work. Time to run some SQL
// queries.
break;
}
}
// Commit the transaction
if( !sqlAccess.BCommitTransaction( true ) )
{
// the transaction failed. Put those caches back on the TODO list
EmitError( SPEW_GC, "CGCBase::YieldingWritebackDirtyCaches() - Writeback failed\n" );
m_vecCacheWritebacks.AddMultipleToTail( vecCachesWritten.Count(), vecCachesWritten.Base() );
FOR_EACH_VEC( vecCachesWritten, nCache )
{
CGCSharedObjectCache *pSOCache = vecCachesWritten[nCache];
UnlockSteamID( pSOCache->GetOwner() );
}
return false;
}
else
{
// the transaction was successful. Tell those caches to forget their dirtiness
FOR_EACH_VEC( vecCachesWritten, nCache )
{
CGCSharedObjectCache *pSOCache = vecCachesWritten[nCache];
pSOCache->SetInWriteback( false );
UnlockSteamID( pSOCache->GetOwner() );
}
return true;
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CGCBase::AddCacheToWritebackQueue( CGCSharedObjectCache *pSOCache )
{
Assert( pSOCache );
if ( ( g_pJobCur != NULL ) && PJobHoldingLock( pSOCache->GetOwner() ) != g_pJobCur && !GGCBase()->BIsSOCacheBeingLoaded( pSOCache->GetOwner() ) )
{
AssertMsg2( false, "CGCBase::AddCacheToWritebackQueue called by job %s for %s, but job does not own lock", g_pJobCur->GetName(), pSOCache->GetOwner().Render() );
}
if( !pSOCache->GetInWriteback() )
{
m_vecCacheWritebacks.AddToTail( pSOCache );
pSOCache->SetInWriteback( true );
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingRetrieveCacheVersion( CGCSharedObjectCache *pSOCache )
{
if ( !socache_persist_version_via_memcached.GetBool() )
{
// We'll keep doing the updates, but fail to restore it if not requested.
return false;
}
CFmtStr1024 key( "SOCacheVersionV2_%llu", pSOCache->GetOwner().ConvertToUint64() );
GCMemcachedGetResult_t data;
if ( !BYieldingMemcachedGet( key.Access(), data ) || !data.m_bKeyFound || sizeof( uint64 ) != data.m_bufValue.Count() )
{
#ifdef _DEBUG
EmitInfo( SPEW_CONSOLE, 3, 3, "SOCacheVersion - Failed to retrieve SO Cache version for: %s\n", pSOCache->GetOwner().Render() );
#endif
return false;
}
//we have a memcached version, so make sure that our version matches what was stored in memcache
uint64 unVersion = *( (uint64 *)data.m_bufValue.Base() );
pSOCache->SetVersion( unVersion );
#ifdef _DEBUG
EmitInfo( SPEW_CONSOLE, 3, 3, "SOCacheVersion::Load - Loaded version from memcached for %s (%llu)\n", pSOCache->GetOwner().Render(), pSOCache->GetVersion() );
#endif
return true;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CGCBase::AddCacheToVersionChangedList( CGCSharedObjectCache *pSOCache )
{
m_rbtreeSOCachesWithDirtyVersions.InsertIfNotFound( pSOCache->GetOwner() );
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CGCBase::UpdateSOCacheVersions()
{
CUtlVector<CUtlString> vecSetKeys( 0, m_rbtreeSOCachesWithDirtyVersions.Count() );
CUtlVector<GCMemcachedBuffer_t> vecSetValues( 0, m_rbtreeSOCachesWithDirtyVersions.Count() );
CUtlBuffer bufData( 0, ( sizeof( uint64 ) * m_rbtreeSOCachesWithDirtyVersions.Count() ) + 1 );
CUtlVector<CUtlString> vecDeleteKeys( 0, m_rbtreeSOCachesWithDirtyVersions.Count() );
for ( int idx = 0; idx < m_rbtreeSOCachesWithDirtyVersions.MaxElement(); ++idx )
{
if ( !m_rbtreeSOCachesWithDirtyVersions.IsValidIndex( idx ) )
continue;
const CSteamID &steamID = m_rbtreeSOCachesWithDirtyVersions[idx];
// if the SO Cache is being loaded, ignore
if ( m_rbtreeSOCachesBeingLoaded.Find( steamID ) != m_rbtreeSOCachesBeingLoaded.InvalidIndex() )
continue;
CSharedObjectCache *pSOCache = FindSOCache( steamID );
if ( pSOCache )
{
CUtlString &strKey = vecSetKeys[ vecSetKeys.AddToTail() ];
strKey.Format( "SOCacheVersionV2_%llu", steamID.ConvertToUint64() );
GCMemcachedBuffer_t &bufVal = vecSetValues[ vecSetValues.AddToTail() ];
bufVal.m_pubData = (byte *)bufData.Base() + bufData.TellPut();
bufVal.m_cubData = sizeof( uint64 );
bufData.PutInt64( pSOCache->GetVersion() );
#ifdef _DEBUG
EmitInfo( SPEW_CONSOLE, 3, 3, "SOCacheVersion - storing version in memcached for %s (%llu).\n", steamID.Render(), pSOCache->GetVersion() );
#endif
}
else
{
// SO Cache is gone, so to be safe, remove the cached version number from memcached
CUtlString &strKey = vecDeleteKeys[ vecDeleteKeys.AddToTail() ];
strKey.Format( "SOCacheVersionV2_%llu", steamID.ConvertToUint64() );
#ifdef _DEBUG
EmitInfo( SPEW_CONSOLE, 3, 3, "SOCacheVersion - no SO Cache, removing version in memcached for %s.\n", steamID.Render() );
#endif
}
}
if ( vecSetKeys.Count() > 0 )
{
BMemcachedSet( vecSetKeys, vecSetValues );
}
if ( vecDeleteKeys.Count() > 0 )
{
BMemcachedDelete( vecDeleteKeys );
}
m_rbtreeSOCachesWithDirtyVersions.RemoveAll();
}
//-----------------------------------------------------------------------------
// Purpose: Returns the publisher access key for Steam Web APIs. This is just
// a stub and must be implimented by a child class if they want this
// funtionality.
//-----------------------------------------------------------------------------
const char *CGCBase::GetSteamAPIKey()
{
AssertMsg( false, "GetWebAPIKey(): Implement me!" );
EmitError( SPEW_CONSOLE, "GetWebAPIKey(): Implement me!\n" );
return "InvalidKey";
}
//-----------------------------------------------------------------------------
// Purpose: Returns true if the protobuf object was stored successfully, false otherwise
//-----------------------------------------------------------------------------
bool CGCBase::BMemcachedSet( const char *pKey, const ::google::protobuf::Message &protoBufObj )
{
// build key
CUtlVector< CUtlString > vecKeys;
int idx = vecKeys.AddToTail();
vecKeys[idx].Set( pKey );
// allocate buffer we will use to stuff into the memcached buffer
CUtlVector< CGCBase::GCMemcachedBuffer_t > vecValues;
uint32 unSize = protoBufObj.ByteSize();
void *pvBuf = stackalloc( unSize );
protoBufObj.SerializeWithCachedSizesToArray( (uint8*)pvBuf );
// stuff the data into the memcached buffer
CGCBase::GCMemcachedBuffer_t buffer;
buffer.m_pubData = pvBuf;
buffer.m_cubData = unSize;
vecValues.AddToTail( buffer );
return BMemcachedSet( vecKeys, vecValues );
}
//-----------------------------------------------------------------------------
// Purpose: Returns true if the memcached value stored via pKey was removed succesfully, false otherwise
//-----------------------------------------------------------------------------
bool CGCBase::BMemcachedDelete( const char *pKey )
{
CUtlVector< CUtlString > vecKeys;
int idx = vecKeys.AddToTail();
vecKeys[idx].Set( pKey );
return BMemcachedDelete( vecKeys );
}
//-----------------------------------------------------------------------------
// Purpose: Returns true if the protobuf object was retrieved from memcached successfully, false otherwise
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingMemcachedGet( const char *pKey, ::google::protobuf::Message &protoBufMsg )
{
// build key
CUtlVector< CUtlString > vecKeys;
int idx = vecKeys.AddToTail();
vecKeys[idx].Set( pKey );
// get results
CUtlVector< CGCBase::GCMemcachedGetResult_t > vecResults;
if ( !BYieldingMemcachedGet( vecKeys, vecResults ) || vecResults.Count() != 1 || vecResults[0].m_bKeyFound == false )
{
return false;
}
if ( !protoBufMsg.ParseFromArray( vecResults[0].m_bufValue.Base(), vecResults[0].m_bufValue.Count() ) )
{
return false;
}
if ( !protoBufMsg.IsInitialized() )
{
return false;
}
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Set the keys and values into memcached
//-----------------------------------------------------------------------------
bool CGCBase::BMemcachedSet( const CUtlVector<CUtlString> &vecKeys, const CUtlVector<GCMemcachedBuffer_t> &vecValues )
{
Assert( vecKeys.Count() == vecValues.Count() );
if ( vecKeys.Count() != vecValues.Count() )
return false;
CProtoBufMsg<CGCMsgMemCachedSet> msgRequest( k_EGCMsgMemCachedSet );
for ( int i = 0; i < vecKeys.Count(); ++i )
{
CGCMsgMemCachedSet_KeyPair *keypair = msgRequest.Body().add_keys();
keypair->set_name( vecKeys[i].String() );
keypair->set_value( vecValues[i].m_pubData, vecValues[i].m_cubData );
}
if( !BSendSystemMessage( msgRequest ) )
return false;
// There is no reply to setting in memcached
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Overload for a single key/value
//-----------------------------------------------------------------------------
bool CGCBase::BMemcachedSet( const CUtlString &strKey, const CUtlBuffer &buf )
{
CUtlVector<CUtlString> memcachedMemberKeys( 0, 1 );
CUtlVector<CGCBase::GCMemcachedBuffer_t> memcachedMemberValues( 0, 1 );
memcachedMemberKeys.AddToTail( strKey );
CGCBase::GCMemcachedBuffer_t &memcachedBuffer = memcachedMemberValues[ memcachedMemberValues.AddToTail() ];
memcachedBuffer.m_pubData = buf.Base();
memcachedBuffer.m_cubData = buf.TellPut();
return BMemcachedSet( memcachedMemberKeys, memcachedMemberValues );
}
//-----------------------------------------------------------------------------
// Purpose: Delete the keys in memcached
//-----------------------------------------------------------------------------
bool CGCBase::BMemcachedDelete( const CUtlVector<CUtlString> &vecKeys )
{
CProtoBufMsg<CGCMsgMemCachedDelete> msgRequest( k_EGCMsgMemCachedDelete );
for ( int i = 0; i < vecKeys.Count(); ++i )
{
msgRequest.Body().add_keys( vecKeys[i].String() );
}
if( !BSendSystemMessage( msgRequest ) )
return false;
// There is no reply to deleting in memcached
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Overload for a single key/value
//-----------------------------------------------------------------------------
bool CGCBase::BMemcachedDelete( const CUtlString &strKey )
{
CUtlVector<CUtlString> vecKeys( 0, 1 );
vecKeys.AddToTail( strKey );
return BMemcachedDelete( vecKeys );
}
//-----------------------------------------------------------------------------
// Purpose: Get the key's values from memcached
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingMemcachedGet( const CUtlVector<CUtlString> &vecKeys, CUtlVector<GCMemcachedGetResult_t> &vecResults )
{
CProtoBufMsg<CGCMsgMemCachedGet> msgRequest( k_EGCMsgMemCachedGet );
for ( int i = 0; i < vecKeys.Count(); ++i )
{
msgRequest.Body().add_keys( vecKeys[i].String() );
}
msgRequest.ExpectingReply( GJobCur().GetJobID() );
if( !BSendSystemMessage( msgRequest ) )
return false;
CProtoBufMsg<CGCMsgMemCachedGetResponse> msgResponse;
if( !GJobCur().BYieldingWaitForMsg( &msgResponse, k_EGCMsgMemCachedGetResponse ) )
{
EmitWarning( SPEW_GC, LOG_ALWAYS, "Didn't get reply from IS for BYieldingMemcachedGet\n" );
return false;
}
Assert( msgRequest.Body().keys_size() == msgResponse.Body().values_size() );
if ( msgRequest.Body().keys_size() != msgResponse.Body().values_size() )
{
EmitWarning( SPEW_GC, LOG_ALWAYS, "Mismatched reply from IS for BYieldingMemcachedGet, asked for %d keys, got %d back\n", (int)msgRequest.Body().keys_size(), (int)msgResponse.Body().values_size() );
return false; // Doesn't match what we asked for!
}
vecResults.Purge();
vecResults.EnsureCapacity( msgResponse.Body().values_size() );
for ( int i = 0; i < msgResponse.Body().values_size(); ++i )
{
GCMemcachedGetResult_t &result = vecResults[ vecResults.AddToTail() ];
result.m_bKeyFound = msgResponse.Body().values(i).found();
if ( result.m_bKeyFound )
{
result.m_bufValue.Copy( &(*msgResponse.Body().values(i).value().begin()), msgResponse.Body().values(i).value().size() );
}
}
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Overload for a single key/value
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingMemcachedGet( const CUtlString &strKeys, GCMemcachedGetResult_t &result )
{
CUtlVector<CUtlString> memcachedMemberKeys( 0, 1 );
CUtlVector<GCMemcachedGetResult_t> memcachedResults;
memcachedMemberKeys.AddToTail( strKeys );
bool bRet = BYieldingMemcachedGet( memcachedMemberKeys, memcachedResults );
if ( !bRet )
return false;
Assert( 1 == memcachedResults.Count() );
if ( 1 != memcachedResults.Count() )
return false;
result.m_bKeyFound = memcachedResults[0].m_bKeyFound;
result.m_bufValue.Swap( memcachedResults[0].m_bufValue );
return true;
}
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingGetIPLocations( CUtlVector<uint32> &vecIPs, CUtlVector<CIPLocationInfo> &infos )
{
CProtoBufMsg<CGCMsgGetIPLocation> msgRequest( k_EGCMsgGetIPLocation );
FOR_EACH_VEC( vecIPs, i )
{
msgRequest.Body().add_ips( vecIPs[i] );
}
msgRequest.ExpectingReply( GJobCur().GetJobID() );
if( !BSendSystemMessage( msgRequest ) )
return false;
// We don't need to worry about a reply mismatch in this case. The message
// has sufficient data so that we can match up the reply properly.
GJobCur().ClearFailedToReceivedMsgType( k_EGCMsgGetIPLocationResponse );
CProtoBufMsg<CGCMsgGetIPLocationResponse> msgResponse;
if( !GJobCur().BYieldingWaitForMsg( &msgResponse, k_EGCMsgGetIPLocationResponse ) )
{
EmitWarning( SPEW_GC, LOG_ALWAYS, "Didn't get reply from IS for BYieldingGetIPLocation\n" );
return false;
}
for ( int i = 0; i < msgResponse.Body().infos_size(); i++ )
{
infos.AddToTail( msgResponse.Body().infos( i ) );
}
return true;
}
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingUpdateGeoLocation( CUtlVector<CSteamID> const &requestedVecSteamIds )
{
CUtlVector<uint32> vecIPs;
CUtlVector<CSteamID> vecSteamIds;
FOR_EACH_VEC( requestedVecSteamIds, i )
{
const CSteamID memberSteamID = requestedVecSteamIds[i];
CGCSession *pSession = FindUserOrGSSession( memberSteamID );
if( pSession )
{
if ( !pSession->GetIPPublic() )
{
EmitInfo( SPEW_GC, 4, LOG_ALWAYS, "BYieldingUpdateGeoLocation Session %s IP == 0, unable to retrieve\n", memberSteamID.Render() ) ;
continue;
}
if ( !pSession->HasGeoLocation() )
{
vecIPs.AddToTail( pSession->GetIPPublic() );
vecSteamIds.AddToTail( memberSteamID );
}
}
}
if (!vecIPs.Count())
return true;
#define iptod(x) ((x)>>24&0xff), ((x)>>16&0xff), ((x)>>8&0xff), ((x)&0xff)
FOR_EACH_VEC( vecIPs, i )
{
EmitInfo( SPEW_GC, geolocation_spewlevel.GetInt(), geolocation_loglevel.GetInt(), "BYieldingUpdateGeoLocation GetIPLocation[%d] = (%s,%u.%u.%u.%u)\n", i, vecSteamIds[i].Render(), iptod( vecIPs[i] ) ) ;
}
CUtlVector<CIPLocationInfo> infos;
if ( BYieldingGetIPLocations( vecIPs, infos ) )
{
// The current IS has a bug where the IP will be blank/zero in the replies. If infos.Count() == vecIPs.Count() assume the order is correct
if ( vecSteamIds.Count() == vecIPs.Count() && vecIPs.Count() == infos.Count() )
{
FOR_EACH_VEC( vecSteamIds, i )
{
CGCSession *pSession = FindUserOrGSSession( vecSteamIds[i] );
if ( pSession )
{
EmitInfo( SPEW_GC, geolocation_spewlevel.GetInt(), geolocation_loglevel.GetInt(), "BYieldingUpdateGeoLocation[MATCHED] SetIPLocation[%s(%u.%u.%u.%u)] = (%6.3f,%6.3f)\n", pSession->GetSteamID().Render(), iptod( vecIPs[i] ), infos[i].latitude(), infos[i].longitude() );
pSession->SetGeoLocation( infos[i].latitude(), infos[i].longitude() );
}
}
}
else
{
FOR_EACH_VEC( vecSteamIds, i )
{
FOR_EACH_VEC( infos, j )
{
if ( infos[j].ip() == vecIPs[i] )
{
CGCSession *pSession = FindUserOrGSSession( vecSteamIds[i] );
if ( pSession )
{
EmitInfo( SPEW_GC, 4, LOG_ALWAYS, "BYieldingUpdateGeoLocation[SEARCHED] SetIPLocation[%s(%u.%u.%u.%u)] = (%6.3f,%6.3f)\n", pSession->GetSteamID().Render(), iptod( vecIPs[i] ), infos[j].latitude(), infos[j].longitude() );
pSession->SetGeoLocation( infos[j].latitude(), infos[j].longitude() );
}
}
}
}
}
return true;
}
return false;
}
//-----------------------------------------------------------------------------
// Purpose: Populate the KeyValues with the stats
//-----------------------------------------------------------------------------
void CGCBase::SystemStats_Update( CGCMsgGetSystemStatsResponse &msgStats )
{
msgStats.set_active_jobs( m_JobMgr.CountJobs() );
msgStats.set_yielding_jobs( m_JobMgr.CountYieldingJobs() );
msgStats.set_user_sessions( m_hashUserSessions.Count() );
msgStats.set_game_server_sessions( m_hashGSSessions.Count() );
msgStats.set_socaches( m_mapSOCache.Count() );
msgStats.set_socaches_to_unload( m_listCachesToUnload.Count() );
msgStats.set_socaches_loading( m_rbtreeSOCachesBeingLoaded.Count() );
msgStats.set_writeback_queue( m_vecCacheWritebacks.Count() );
msgStats.set_steamid_locks( m_hashSteamIDLocks.Count() );
msgStats.set_logon_queue( m_llStartPlaying.Count() );
msgStats.set_logon_jobs( m_nStartPlayingJobCount );
}
//-----------------------------------------------------------------------------
// Purpose: Returns the singleton GC object
//-----------------------------------------------------------------------------
CGCBase *GGCBase()
{
return g_pGCBase;
}
//-----------------------------------------------------------------------------
// Purpose: Spews information about the active locks on the GC
//-----------------------------------------------------------------------------
int LockSortFunc( CLock * const *lhs, CLock * const *rhs )
{
return (*rhs)->GetWaitingCount() - (*lhs)->GetWaitingCount();
}
void CGCBase::DumpSteamIDLocks( bool bFull, int nMax )
{
CUtlVector<CLock *> vecLocks;
for( CLock *pLock = m_hashSteamIDLocks.PvRecordFirst(); NULL != pLock; pLock = m_hashSteamIDLocks.PvRecordNext( pLock ) )
{
if( pLock->BIsLocked() )
{
vecLocks.AddToTail( pLock );
}
}
vecLocks.Sort( LockSortFunc );
if( nMax > vecLocks.Count() || bFull )
{
nMax = vecLocks.Count();
}
EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "%d locks total. %d locked, %d displayed\n", m_hashSteamIDLocks.Count(), vecLocks.Count(), nMax );
EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "Lock Holding Job First Waiting Job Wait Count Lock Time\n" );
for( int nLock = 0; nLock < nMax; nLock++ )
{
CLock *pLock = vecLocks[nLock];
EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "%-24s %-22s %-22s %-11d %d\n",
pLock->GetName(),
pLock->GetJobLocking() ? pLock->GetJobLocking()->GetName() : "--",
pLock->GetJobWaitingQueueHead() ? pLock->GetJobWaitingQueueHead()->GetName() : "--",
pLock->GetWaitingCount(),
(int) ( pLock->GetMicroSecondsSinceLock() / k_nMillion ) );
}
}
//-----------------------------------------------------------------------------
// Purpose: Dumps informations about currently running jobs
//-----------------------------------------------------------------------------
void CGCBase::DumpJobs( const char *pszJobName, int nMax, int nPrintLocksMax ) const
{
m_JobMgr.DumpJobs( pszJobName, nMax, nPrintLocksMax );
}
//-----------------------------------------------------------------------------
// Purpose: Dumps information about a specific job
//-----------------------------------------------------------------------------
void CGCBase::DumpJob( JobID_t jobID, int nPrintLocksMax ) const
{
m_JobMgr.DumpJob( jobID, nPrintLocksMax );
}
//-----------------------------------------------------------------------------
// Purpose: Returns counts of core objects
//-----------------------------------------------------------------------------
int CGCBase::GetSOCacheCount() const
{
return m_mapSOCache.Count();
}
bool CGCBase::IsSOCached( const CSharedObject *pObj, uint32 nTypeID ) const
{
// OPT: If there are many caches, this is very slow - it would be faster have a ref count on the shared object to track this.
// However this is debug only code.
#if defined( DEBUG )
FOR_EACH_MAP_FAST( m_mapSOCache, i )
{
CGCSharedObjectCache *pCache = m_mapSOCache[ i ];
if ( pCache->IsObjectCached( pObj, nTypeID ) )
{
return true;
}
if ( pCache->IsObjectDirty( pObj ) )
{
Assert( false );
return true;
}
}
#else
AssertMsg( false, "Calling IsSOCached() in release mode. This is a debug only function" );
#endif
return false;
}
int CGCBase::GetUserSessionCount() const
{
return m_hashUserSessions.Count();
}
int CGCBase::GetGSSessionCount() const
{
return m_hashGSSessions.Count();
}
//-----------------------------------------------------------------------------
// Purpose: Mark that we are shutting down
//-----------------------------------------------------------------------------
void CGCBase::SetIsShuttingDown()
{
m_bIsShuttingDown = true;
GetJobMgr().SetIsShuttingDown();
}
//-----------------------------------------------------------------------------
// Purpose: Sets whether we are profiling or not
//-----------------------------------------------------------------------------
void CGCBase::SetProfilingEnabled( bool bEnabled )
{
if ( bEnabled )
{
m_bStartProfiling = true;
}
else
{
m_bStopProfiling = true;
}
}
//-----------------------------------------------------------------------------
// Purpose: Sets whether to spew about vprof imbalances
//-----------------------------------------------------------------------------
void CGCBase::SetDumpVprofImbalances( bool bEnabled )
{
m_bDumpVprofImbalances = bEnabled;
}
//-----------------------------------------------------------------------------
// Purpose: Returns whether we are spewing vprof imbalances
//-----------------------------------------------------------------------------
bool CGCBase::GetVprofImbalances()
{
return m_bDumpVprofImbalances;
}
//-----------------------------------------------------------------------------
// Purpose: Returns a steam ID for a user-provided input. Works with accountID,
// steam account name, or steam ID.
//-----------------------------------------------------------------------------
CSteamID CGCBase::YieldingGuessSteamIDFromInput( const char *pchInput )
{
AssertRunningJob();
if( !pchInput )
{
EmitError( SPEW_CONSOLE, "Invalid NULL string passed to YieldingGuessSteamIDFromInput\n" );
return CSteamID();
}
EUniverse localUniverse = m_pHost->GetUniverse();
// Is it a 64 bit Steam ID?
if ( pchInput[0] >= '0' && pchInput[0] <= '9' )
{
CSteamID steamID( V_atoui64( pchInput ) );
if ( steamID.IsValid() )
return steamID;
}
// quoted
// See if it's a profile link. If it is, clip the SteamID from it.
const char *pszProfilePrepend = "steamcommunity.com/profiles/";
int iInputLen = Q_strlen(pchInput);
int iProfilePrependLen = Q_strlen(pszProfilePrepend);
const char *pszFound = NULL;
if ( (pszFound = Q_stristr( pchInput, pszProfilePrepend )) != NULL )
{
if ( iInputLen > ((pszFound + iProfilePrependLen) - pchInput) )
{
CSteamID steamID;
steamID.SetFromString( (pszFound + iProfilePrependLen), localUniverse );
if ( steamID.IsValid() )
return steamID;
}
}
// See if it's an id link.
const char *pszIDPrepend = "steamcommunity.com/id/";
int iIDPrependLen = Q_strlen(pszIDPrepend);
if ( (pszFound = Q_stristr( pchInput, pszIDPrepend )) != NULL )
{
if ( iInputLen > ((pszFound + iIDPrependLen) - pchInput) )
{
char szMaxURL[512];
Q_strncpy( szMaxURL, (pszFound + iIDPrependLen), sizeof(szMaxURL) );
// Trim off a trailing slash
int iURLLen = Q_strlen(szMaxURL);
if ( szMaxURL[iURLLen-1] == '/' || pchInput[iURLLen-1] == '\\' )
{
szMaxURL[iURLLen-1] = '\0';
}
CUtlVector< CSteamID > vecIDs;
if ( BYieldingLookupAccount( k_EFindAccountTypeURL, szMaxURL, &vecIDs ) )
{
// Should only ever find a single account for a URL
if ( vecIDs.Count() == 1 )
return vecIDs[0];
}
}
}
CGCMsg< MsgGCEmpty_t > msg( k_EGCMsgLookupAccountFromInput );
msg.AddStrData( pchInput );
msg.ExpectingReply( GJobCur().GetJobID() );
if( !BSendSystemMessage( msg ) )
{
EmitError( SPEW_CONSOLE, "Unable to query GCHost in YieldingGuessSteamIDFromInput\n" );
return CSteamID();
}
CGCMsg< MsgGCLookupAccountResponse > msgReply;
if( !GJobCur().BYieldingWaitForMsg( &msgReply, k_EGCMsgGenericReply ) )
{
EmitError( SPEW_CONSOLE, "No response from GCHost in YieldingGuessSteamIDFromInput\n" );
return CSteamID();
}
return CSteamID( msgReply.Body().m_ulSteamID );
}
//-----------------------------------------------------------------------------
// Purpose: Returns all matching Steam IDs for the specified query.
// Returns: true if a response was received from Steam. The list may still be
// empty in that case.
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingLookupAccount( EAccountFindType eFindType, const char *pchInput, CUtlVector< CSteamID > *prSteamIDs )
{
if ( eFindType == k_EFindAccountTypeURL )
{
CSteamAPIRequest apiRequest( k_EHTTPMethodGET, "ISteamUser", "ResolveVanityURL", 1 );
apiRequest.SetGETParamString( "vanityurl", pchInput );
KeyValuesAD kvAPIResponse( "response" );
CUtlString sWebApiErrMsg;
EResult eResult = YieldingSendWebAPIRequest( apiRequest, kvAPIResponse, sWebApiErrMsg, false );
if ( k_EResultOK != eResult )
{
// Emit an error on the less-common errors
if ( k_EResultNoMatch != eResult )
{
EmitError( SPEW_GC, "WebAPI error looking up vanity URL by GC. %s\n", sWebApiErrMsg.String() );
}
return false;
}
prSteamIDs->AddToTail( CSteamID( kvAPIResponse->GetUint64( "steamid" ) ) );
return true;
}
else
{
CProtoBufMsg< CMsgAMFindAccounts > msg( k_EGCMsgFindAccounts );
msg.Body().set_search_type( eFindType );
msg.Body().set_search_string( pchInput );
msg.ExpectingReply( GJobCur().GetJobID() );
if( !BSendSystemMessage( msg ) )
{
EmitError( SPEW_GC, "Unable to send GCMsgFindAccounts\n" );
return false;
}
CProtoBufMsg< CMsgAMFindAccountsResponse > response;
if( !GJobCur().BYieldingWaitForMsg( &response, k_EGCMsgGenericReply ) )
{
EmitError( SPEW_GC, "No response to GCMsgFindAccounts\n" );
return false;
}
for( int i=0; i<response.Body().steam_id_size(); i++ )
{
prSteamIDs->AddToTail( CSteamID( response.Body().steam_id( i ) ) );
}
return true;
}
}
GC_CON_COMMAND( gc_search_vanityurl, "Tests searching for an account by vanity URL" )
{
CUtlVector< CSteamID > vecIDs;
if ( GGCBase()->BYieldingLookupAccount( k_EFindAccountTypeURL, args[1], &vecIDs ) )
{
Msg( "Search success.\n" );
FOR_EACH_VEC( vecIDs, i )
{
CSteamID result = vecIDs[i];
Msg( "Result: %llu\n", result.ConvertToUint64() );
}
}
else
{
Msg( "Search failure.\n" );
}
}
//-----------------------------------------------------------------------------
// Purpose: Dumps a summary of the GC's status
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingRecordSupportAction( const CSteamID & actorID, const CSteamID & targetID, const char *pchData, const char *pchNote )
{
CGCMsg< MsgGCRecordSupportAction_t > msgRecordSupportAction( k_EGCMsgRecordSupportAction );
msgRecordSupportAction.Body().m_unAccountID = targetID.GetAccountID();
msgRecordSupportAction.Body().m_unActorID = actorID.GetAccountID();
msgRecordSupportAction.AddStrData( pchData );
msgRecordSupportAction.AddStrData( pchNote );
msgRecordSupportAction.ExpectingReply( GJobCur().GetJobID() );
GGCBase()->BSendSystemMessage( msgRecordSupportAction );
CGCMsg< MsgGCEmpty_t > msgReply;
if( !GJobCur().BYieldingWaitForMsg( &msgReply, k_EGCMsgGenericReply ) )
{
EmitError( SPEW_GC, "No reply received to support action message\n" );
return false;
}
else
{
return true;
}
}
//-----------------------------------------------------------------------------
// Purpose: Posts a steam alert to the alert alias for this GC's app.
//-----------------------------------------------------------------------------
void CGCBase::PostAlert( EAlertType eAlertType, bool bIsCritical, const char *pchAlertText, const CUtlVector< CUtlString > *pvecExtendedInfo, bool bAlsoSpew )
{
CProtoBufMsg< CMsgNotifyWatchdog > msg( k_EGCMsgPostAlert );
msg.Body().set_alert_type( eAlertType );
msg.Body().set_critical( bIsCritical );
if( !pvecExtendedInfo )
{
msg.Body().set_text( pchAlertText );
}
else
{
// put all the messages in one giant string and set that as the text
// figure out how big "giant" is
uint32 unSize = Q_strlen( pchAlertText ) + 2; // header + \n + null
FOR_EACH_VEC( *pvecExtendedInfo, nLine )
{
unSize += pvecExtendedInfo->Element( nLine ).Length();
}
// walk the strings again to assemble the buffer
CUtlBuffer bufMessage( 0, unSize, CUtlBuffer::TEXT_BUFFER );
bufMessage.PutString( pchAlertText );
bufMessage.PutString( "\n" );
FOR_EACH_VEC( *pvecExtendedInfo, nLine )
{
bufMessage.PutString( pvecExtendedInfo->Element( nLine ).Get() );
}
msg.Body().set_text( (const char *)bufMessage.Base() );
}
if( bAlsoSpew )
{
EmitError( SPEW_GC, "%s", msg.Body().text().c_str() );
}
BSendSystemMessage( msg );
}
//-----------------------------------------------------------------------------
// Purpose: Fills the vector with all package IDs this account has a license to
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingGetAccountLicenses( const CSteamID & steamID, CUtlVector< PackageLicense_t > & vecPackages )
{
CProtoBufMsg< CMsgAMGetLicenses > msg( k_EGCMsgGetLicenses );
msg.Body().set_steamid( steamID.ConvertToUint64() );
msg.ExpectingReply( GJobCur().GetJobID() );
if( !BSendSystemMessage( msg ) )
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "Unable to send GetAccountLicenses system message\n" );
return false;
}
CProtoBufMsg< CMsgAMGetLicensesResponse > msgReply;
if( !GJobCur().BYieldingWaitForMsg( &msgReply, k_EGCMsgGenericReply ) )
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "Timeout waiting for GetAccountLicenses reply\n" );
return false;
}
if( msgReply.Body().result() != k_EResultOK )
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "GetAccountLicenses for %s failed with %d\n", steamID.Render(), msgReply.Body().result() );
return false;
}
vecPackages.RemoveAll();
vecPackages.EnsureCapacity( msgReply.Body().license_size() );
for( int i=0; i < msgReply.Body().license_size(); i++ )
{
const CMsgPackageLicense &msgPackage = msgReply.Body().license( i );
//skip packages that they directly don't own (they may be lent to them via library sharing, and we don't want to grant based on that).
//we count account ID of zero as matching so we can deal with old Steam versions that didn't provide this field
if( ( msgPackage.owner_id() != steamID.GetAccountID() ) && ( msgPackage.owner_id() != 0 ) )
continue;
PackageLicense_t package;
package.m_unPackageID = msgPackage.package_id();
package.m_rtimeCreated = msgPackage.time_created();
vecPackages.AddToTail( package );
}
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Fills the vector with all package IDs this account has a license to
//-----------------------------------------------------------------------------
bool CGCBase::BYieldingAddFreeLicense( const CSteamID & steamID, uint32 unPackageID, uint32 unIPPublic, const char *pchStoreCountryCode )
{
CProtoBufMsg< CMsgAMAddFreeLicense > msg( k_EGCMsgAddFreeLicense );
msg.Body().set_steamid( steamID.ConvertToUint64() );
msg.Body().set_packageid( unPackageID );
if( unIPPublic )
msg.Body().set_ip_public( unIPPublic );
if( pchStoreCountryCode )
msg.Body().set_store_country_code( pchStoreCountryCode );
msg.ExpectingReply( GJobCur().GetJobID() );
if( !BSendSystemMessage( msg ) )
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "Unable to send GetAccountLicenses system message\n" );
return false;
}
CProtoBufMsg< CMsgAMAddFreeLicenseResponse > msgReply;
if( !GJobCur().BYieldingWaitForMsg( &msgReply, k_EGCMsgAddFreeLicenseResponse ) )
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "Timeout waiting for GetAccountLicenses reply\n" );
return false;
}
if( msgReply.Body().eresult() != k_EResultOK )
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "BYieldingAddFreeLicense for %s failed with %d\n", steamID.Render(), msgReply.Body().eresult() );
return false;
}
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Fills the vector with all package IDs this account has a license to
//-----------------------------------------------------------------------------
int CGCBase::YieldingGrantGuestPass( const CSteamID & steamID, uint32 unPackageID, uint32 unPassesToGrant, int32 nDaysToExpiration )
{
CProtoBufMsg<CMsgAMGrantGuestPasses2> msg( k_EGCMsgGrantGuestPass );
msg.Body().set_steam_id( steamID.ConvertToUint64() );
msg.Body().set_package_id( unPackageID );
msg.Body().set_passes_to_grant( unPassesToGrant );
msg.Body().set_days_to_expiration( nDaysToExpiration );
msg.ExpectingReply( GJobCur().GetJobID() );
if( !BSendSystemMessage( msg ) )
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "Unable to send GrantGuestPass system message\n" );
return 0;
}
CProtoBufMsg<CMsgAMGrantGuestPasses2Response> msgReply;
if( !GJobCur().BYieldingWaitForMsg( &msgReply, k_EGCMsgGrantGuestPassResponse ) )
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "Timeout waiting for GrantGuestPass reply\n" );
return 0;
}
if( msgReply.Body().eresult() != k_EResultOK )
{
EmitWarning( SPEW_GC, SPEW_ALWAYS, "YieldingGrantGuestPass for %s failed with %d\n", steamID.Render(), msgReply.Body().eresult() );
return 0;
}
return msgReply.Body().passes_granted();
}
//-----------------------------------------------------------------------------
// Purpose: Gets data for an account
//-----------------------------------------------------------------------------
const CAccountDetails *CGCBase::YieldingGetAccountDetails( const CSteamID & steamID, bool bForceReload )
{
return m_AccountDetailsManager.YieldingGetAccountDetails( steamID, bForceReload );
}
//-----------------------------------------------------------------------------
// Purpose: Gets the current persona name for an account
//-----------------------------------------------------------------------------
const char *CGCBase::YieldingGetPersonaName( const CSteamID & steamID, const char *szUnknownName )
{
const char *szPersonaName = m_AccountDetailsManager.YieldingGetPersonaName( steamID );
return szPersonaName ? szPersonaName : szUnknownName;
}
//-----------------------------------------------------------------------------
// Purpose: Clears a persona name from the cache
//-----------------------------------------------------------------------------
void CGCBase::ClearCachedPersonaName( const CSteamID & steamID )
{
m_AccountDetailsManager.ClearCachedPersonaName( steamID );
}
//-----------------------------------------------------------------------------
// Purpose: Tells us to load the persona name for a user, but not wait on it
//-----------------------------------------------------------------------------
void CGCBase::PreloadPersonaName( const CSteamID & steamID )
{
m_AccountDetailsManager.PreloadPersonaName( steamID );
}
//-----------------------------------------------------------------------------
// Purpose: Sends a message to the web API servers letting them know what the
// methods and interfaces are for this GC.
//-----------------------------------------------------------------------------
bool CGCBase::BSendWebApiRegistration()
{
// if we aren't initialized enough to have a GCHost, just skip this
// registration request. We'll register later in our init process.
if( !m_pHost )
return false;
if( CGCWebAPIInterfaceMapRegistrar::VecInstance().Count() > 0 )
{
CGCMsg< MsgGCWebAPIRegisterInterfaces_t > msgWebRegistration( k_EGCMsgWebAPIRegisterInterfaces );
msgWebRegistration.Body().m_cInterfaces = CGCWebAPIInterfaceMapRegistrar::VecInstance().Count();
CUtlBuffer bufRegistrations;
FOR_EACH_VEC( CGCWebAPIInterfaceMapRegistrar::VecInstance(), nInterface )
{
KeyValues *pkvInterface = CGCWebAPIInterfaceMapRegistrar::VecInstance()[ nInterface ]();
Assert( pkvInterface );
if( !pkvInterface )
return false;
KVPacker packer;
packer.WriteAsBinary( pkvInterface, bufRegistrations );
pkvInterface->deleteThis();
}
msgWebRegistration.AddVariableLenData( bufRegistrations.Base(), bufRegistrations.TellPut() );
if( !BSendSystemMessage( msgWebRegistration ) )
return false;
}
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Dumps a summary of the GC's status
//-----------------------------------------------------------------------------
void CGCBase::Dump() const
{
char rtimeBuf[k_RTimeRenderBufferSize];
EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "GC Status for %d: path=%s\n", m_unAppID, m_sPath.Get() );
EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\tLogon Surge: %s\n", BIsInLogonSurge() ? "Yes" : "No" );
EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\tStartPlaying: waiting=%d, jobs running=%d of %d\n", m_llStartPlaying.Count(), m_nStartPlayingJobCount, cv_concurrent_start_playing_limit.GetInt() );
EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\tJobs: active=%d, yielding=%d\n", m_JobMgr.CountJobs(), m_JobMgr.CountYieldingJobs() );
EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\tSessions: user=%d, gameserver=%d\n", m_hashUserSessions.Count(), m_hashGSSessions.Count() );
EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\tCaches: %d (%d waiting to unload, %d currently loading, %s %d /+ %d)\n", m_mapSOCache.Count(), m_listCachesToUnload.Count(), m_rbtreeSOCachesBeingLoaded.Count(),
( ( ( m_jobidFlushInventoryCacheAccounts == k_GIDNil ) || !m_JobMgr.BJobExists( m_jobidFlushInventoryCacheAccounts ) ) ? "last flushed" : "currently flushing" ),
m_numFlushInventoryCacheAccountsLastScheduled, m_rbFlushInventoryCacheAccounts.Count() );
EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\tWriteback Queue: %d (oldest: %s)\n", m_vecCacheWritebacks.Count(), m_vecCacheWritebacks.Count() > 0 ? CRTime::Render( m_vecCacheWritebacks[0]->GetWritebackTime(), rtimeBuf ) : "none" );
EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, LOG_ALWAYS, "\tYieldingRequestSession: %d active\n", m_nRequestSessionJobsActive );
m_AccountDetailsManager.Dump();
}
//-----------------------------------------------------------------------------
// Purpose: Dumps a summary of the GC's status
//-----------------------------------------------------------------------------
const char *CGCBase::GetCDNURL() const
{
if( m_sCDNURL.IsEmpty() )
{
switch( m_pHost->GetUniverse() )
{
case k_EUniverseDev:
case k_EUniverseBeta:
m_sCDNURL.Format( "http://cdn.beta.steampowered.com/apps/%d/", GetAppID() );
break;
case k_EUniversePublic:
default:
m_sCDNURL.Format( "http://media.steampowered.com/apps/%d/", GetAppID() );
break;
}
}
return m_sCDNURL.Get();
}
//-----------------------------------------------------------------------------
// Purpose: Prints an assert to the console
//-----------------------------------------------------------------------------
void CGCBase::AssertCallbackFunc( const char *pchFile, int nLine, const char *pchMessage )
{
if ( !ThreadInMainThread() ) // !KLUDGE!
{
EmitWarning( SPEW_GC, 4, "Thread assert %s(%d): %s\n", pchFile, nLine, pchMessage );
return;
}
// Our spew handler should have already spewed this once, no need to spew it again
//EmitError( SPEW_CONSOLE, "%s (%d): %s\n", V_GetFileName( pchFile ), nLine, pchMessage );
if ( !Plat_IsInDebugSession() )
{
char rchCleanedJobName[48] = "";
if ( ThreadInMainThread() && g_pJobCur != NULL )
{
const char *pszJobName = g_pJobCur->GetName();
int l = 0;
while ( l < sizeof(rchCleanedJobName)-1 )
{
char c = pszJobName[l];
if ( c == '\0' )
break;
if ( !V_isalnum( c ) )
{
c = '_';
}
rchCleanedJobName[l] = c;
++l;
}
rchCleanedJobName[l] = 0;
}
// Throttle writing of minidumps on a file / line / job basis
CFmtStr sFileAndLine( "assert_%s(%d)%s%s",
V_GetFileName( pchFile ),
nLine,
rchCleanedJobName[0] ? "_" : "",
rchCleanedJobName
);
static CUtlDict< CCopyableUtlVector< RTime32 > > s_dictAsserts;
int iDict = s_dictAsserts.Find( sFileAndLine.Access() );
if ( !s_dictAsserts.IsValidIndex( iDict ) )
{
iDict = s_dictAsserts.Insert( sFileAndLine.Access() );
}
CCopyableUtlVector< RTime32 > &vecTimes = s_dictAsserts[iDict];
int nStale = 0;
while ( nStale < vecTimes.Count() && ( CRTime::RTime32TimeCur() - vecTimes[nStale] ) > (uint32)cv_assert_minidump_window.GetInt() )
{
nStale++;
}
vecTimes.RemoveMultipleFromHead( nStale );
bool bWriteDump = ( vecTimes.Count() < cv_assert_max_minidumps_in_window.GetInt() );
if ( bWriteDump )
{
vecTimes.AddToTail( CRTime::RTime32TimeCur() );
CUtlString sCurJob;
if ( ThreadInMainThread() && g_pJobCur != NULL )
{
sCurJob.Format( "[From job %s]\n", g_pJobCur->GetName() );
}
// Write the dump
CUtlString sDumpComment;
sDumpComment.Format( "%s%s%s(%d): %s",
GGCBase()->GetIsShuttingDown() ? "[During shutdown]\n" : "", // Asserts during shutdown are much more often spurious. Let's make it clear if a shutdown happens during shutdown
sCurJob.String(), // The name of the current job name is often an incredibly useful piece of info. If the dumps are not valid, this can narrow the search space immensely
pchFile,
nLine,
pchMessage
);
SetMinidumpComment( sDumpComment.String() );
WriteMiniDump( sFileAndLine.Access() );
SetMinidumpComment( "" ); // just for grins
}
}
}
//-----------------------------------------------------------------------------
// Purpose: Claims all the memory for the GC
//-----------------------------------------------------------------------------
void CGCBase::Validate( CValidator &validator, const char *pchName )
{
VPROF_BUDGET( "CGCBase::Validate", VPROF_BUDGETGROUP_STEAM );
// these are INSIDE the function instead of outside so the interface
// doesn't change
#ifdef DBGFLAG_VALIDATE
VALIDATE_SCOPE();
// Validate the global message list
g_theMessageList.Validate( validator, "g_theMessageList" );
// Validate the network global memory pool
g_MemPoolMsg.Validate( validator, "g_MemPoolMsg" );
CNetPacketPool::ValidateGlobals( validator );
CJobMgr::ValidateStatics( validator, "CJobMgr" );
CJob::ValidateStatics( validator, "CJob" );
ValidateTempTextBuffers( validator );
ValidateObj( m_JobMgr );
ValidateObj( m_sPath );
ValidateObj( m_hashUserSessions );
for( CGCUserSession **ppSession = m_hashUserSessions.PvRecordFirst(); ppSession != NULL; ppSession = m_hashUserSessions.PvRecordNext( ppSession ) )
{
ValidatePtr( *ppSession );
}
ValidateObj( m_hashGSSessions );
for( CGCGSSession **ppSession = m_hashGSSessions.PvRecordFirst(); ppSession != NULL; ppSession = m_hashGSSessions.PvRecordNext( ppSession ) )
{
ValidatePtr( *ppSession );
}
// validate the SQL access layer
CRecordBase::ValidateStatics( validator, "CRecordBase" );
GSchemaFull().Validate( validator, "GSchemaFull" );
CRecordInfo::ValidateStatics( validator, "CRecordInfo" );
CSharedObject::ValidateStatics( validator );
OnValidate( validator, pchName );
#endif // DBGFLAG_VALIDATE
}
EResult YieldingSendWebAPIRequest( CSteamAPIRequest &request, KeyValues *pKVResponse, CUtlString &errMsg, bool b200MeansSuccess )
{
CHTTPResponse apiResponse;
if ( !GGCBase()->BYieldingSendHTTPRequest( &request, &apiResponse ) )
{
errMsg.Format( "Did not get a response" );
return k_EResultTimeout;
}
if ( k_EHTTPStatusCode200OK != apiResponse.GetStatusCode() )
{
errMsg.Format( "HTTP status code %d", apiResponse.GetStatusCode() );
// if ( k_EResultOK != pKVResponse->GetInt( "result", k_EResultFail ) )
// {
// EmitError( SPEW_GC, "Web call to %s failed with error %d: %s\n",
// request.GetURL(),
// pKVResponse->GetInt( "error/errorcode", k_EResultFail ),
// pKVResponse->GetString( "error/errordesc" ) );
// return pKVResponse->GetInt( "error/errorcode", k_EResultFail );
// }
return k_EResultFail;
}
if ( apiResponse.GetBodyBuffer() )
{
pKVResponse->UsesEscapeSequences( true );
if ( !pKVResponse->LoadFromBuffer( "webResponse", *apiResponse.GetBodyBuffer() ) )
{
errMsg.Format( "Failed to parse keyvalues result" );
return k_EResultFail;
}
}
if ( b200MeansSuccess )
{
return k_EResultOK;
}
int result = pKVResponse->GetInt( "success", -1 );
if ( result < 0 )
{
errMsg = "Reply missing result code";
return k_EResultFail;
}
errMsg = pKVResponse->GetString( "message", "" );
if ( result != k_EResultOK && errMsg.IsEmpty() )
{
errMsg = "(Unknown error)";
}
return (EResult)result;
}
GC_CON_COMMAND( ip_geolocation, "<a.b.c.d> Perform geolocation lookup" )
{
if ( args.ArgC() < 2 )
{
EmitError( SPEW_GC, "Pass at least one IP to lookup\n" );
return;
}
// Get List of IP's to query
CUtlVector<uint32> vecIPs;
for ( int i = 1 ; i < args.ArgC() ; ++i )
{
netadr_t adr;
adr.SetFromString( args[i] );
if ( adr.GetIPHostByteOrder() == 0 )
{
EmitInfo( SPEW_GC, 1, 1, "%s is not a valid IP\n", args[i] );
}
else
{
vecIPs.AddToTail( adr.GetIPHostByteOrder() );
}
}
if ( vecIPs.Count() <= 0 )
return;
// Do the query
CUtlVector<CIPLocationInfo> vecInfos;
vecInfos.SetCount( vecIPs.Count() );
GGCBase()->BYieldingGetIPLocations( vecIPs, vecInfos );
for ( int i = 0 ; i < vecInfos.Count() ; ++i )
{
netadr_t adr( vecInfos[i].ip(), 0 );
EmitInfo( SPEW_GC, 1, 1, "%s: %.1f, %.1f\n", adr.ToString( true ), vecInfos[i].latitude(), vecInfos[i].longitude() );
}
}
} // namespace GCSDK