mirror of
https://github.com/nillerusr/source-engine.git
synced 2024-12-22 22:27:05 +00:00
4433 lines
151 KiB
C++
4433 lines
151 KiB
C++
//========= 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
|
|
|
|
|
|
|