//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // //============================================================================= #include "cbase.h" #ifdef _WIN32 #include "winerror.h" #endif #include "achievementmgr.h" #include "icommandline.h" #include "KeyValues.h" #include "filesystem.h" #include "inputsystem/InputEnums.h" #include "usermessages.h" #include "fmtstr.h" #include "tier1/utlbuffer.h" #ifdef CLIENT_DLL #include "achievement_notification_panel.h" #include "c_playerresource.h" #include "gamestats.h" #ifdef TF_CLIENT_DLL #include "econ_item_inventory.h" #endif //TF_CLIENT_DLL #else #include "enginecallback.h" #endif // CLIENT_DLL #ifndef _X360 #include "steam/isteamuserstats.h" #include "steam/isteamfriends.h" #include "steam/isteamutils.h" #include "steam/steam_api.h" #include "steam/isteamremotestorage.h" #else #include "xbox/xbox_win32stubs.h" #endif #include "tier3/tier3.h" #include "vgui/ILocalize.h" #ifdef _X360 #include "ixboxsystem.h" #endif // _X360 #include "engine/imatchmaking.h" #include "tier0/vprof.h" #if defined(TF_DLL) || defined(TF_CLIENT_DLL) #include "tf_gamerules.h" #endif ConVar cc_achievement_debug( "achievement_debug", "0", FCVAR_CHEAT | FCVAR_REPLICATED, "Turn on achievement debug msgs." ); #ifdef CSTRIKE_DLL //============================================================================= // HPE_BEGIN: // [Forrest] Allow achievements/stats to be turned off for a server //============================================================================= ConVar sv_nostats( "sv_nostats", "0", FCVAR_REPLICATED | FCVAR_NOTIFY, "Disable collecting statistics and awarding achievements." ); //============================================================================= // HPE_END //============================================================================= #endif // CSTRIKE_DLL const char *COM_GetModDirectory(); extern ConVar developer; #define DEBUG_ACHIEVEMENTS_IN_RELEASE 0 #ifdef SWDS // Hack this for now until we get steam_api recompiling in the Steam codebase. ISteamUserStats *SteamUserStats() { return NULL; } #endif //----------------------------------------------------------------------------- // Purpose: Write helper //----------------------------------------------------------------------------- //============================================================================= // HPE_BEGIN // [dwenger] Steam Cloud Support //============================================================================= static void WriteAchievementGlobalState( KeyValues *pKV, bool bPersistToSteamCloud = false ) //============================================================================= // HPE_END //============================================================================= { #ifdef _X360 if ( XBX_GetStorageDeviceId() == XBX_INVALID_STORAGE_ID || XBX_GetStorageDeviceId() == XBX_STORAGE_DECLINED ) return; #endif char szFilename[_MAX_PATH]; if ( IsX360() ) { Q_snprintf( szFilename, sizeof( szFilename ), "cfg:/%s_GameState.txt", COM_GetModDirectory() ); } else { Q_snprintf( szFilename, sizeof( szFilename ), "GameState.txt" ); } // Never call pKV->SaveToFile!!!! // Save to a buffer instead. CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER ); pKV->RecursiveSaveToFile( buf, 0 ); filesystem->WriteFile( szFilename, NULL, buf ); pKV->deleteThis(); //============================================================================= // HPE_BEGIN // [dwenger] Steam Cloud Support //============================================================================= if ( bPersistToSteamCloud ) { #ifndef NO_STEAM if ( IsX360() ) { Q_snprintf( szFilename, sizeof( szFilename ), "cfg:/%s_GameState.txt", COM_GetModDirectory() ); } else { Q_snprintf( szFilename, sizeof( szFilename ), "GameState.txt" ); } ISteamRemoteStorage *pRemoteStorage = SteamClient()?(ISteamRemoteStorage *)SteamClient()->GetISteamGenericInterface( SteamAPI_GetHSteamUser(), SteamAPI_GetHSteamPipe(), STEAMREMOTESTORAGE_INTERFACE_VERSION ):NULL; if (pRemoteStorage) { int32 availableBytes = 0; int32 totalBytes = 0; if ( pRemoteStorage->GetQuota( &totalBytes, &availableBytes ) ) { if ( totalBytes > 0 ) { int32 filesize = (int32)filesystem->Size(szFilename); if (filesize > 0) { char* pData = new char[filesize]; if (pData) { // Read in the data from the file system GameState.txt file FileHandle_t handle = filesystem->Open(szFilename, "r"); if (handle) { int32 nRead = filesystem->Read(pData, filesize, handle); filesystem->Close(handle); if (nRead == filesize) { // Write out the data to steam cloud pRemoteStorage->FileWrite(szFilename, pData, filesize); } } // Delete the data array delete []pData; } } } } } #endif } //============================================================================= // HPE_END //============================================================================= #ifdef _X360 if ( xboxsystem ) { xboxsystem->FinishContainerWrites(); } #endif } //----------------------------------------------------------------------------- // Purpose: Async save thread //----------------------------------------------------------------------------- class CAchievementSaveThread : public CWorkerThread { public: CAchievementSaveThread() : m_pKV( NULL ) { SetName( "AchievementSaveThread" ); } ~CAchievementSaveThread() { } enum { CALL_FUNC, EXIT, }; void WriteAchievementGlobalState( KeyValues *pKV ) { Assert( !m_pKV ); m_pKV = pKV; CallWorker( CALL_FUNC ); Assert( !m_pKV ); } int Run() { unsigned nCall; while ( WaitForCall( &nCall ) ) { if ( nCall == EXIT ) { Reply( 1 ); break; } KeyValues *pKV = m_pKV; m_pKV = NULL; Reply( 1 ); ::WriteAchievementGlobalState( pKV ); } return 0; } private: KeyValues *m_pKV; }; static CAchievementSaveThread g_AchievementSaveThread; //----------------------------------------------------------------------------- // Purpose: constructor //----------------------------------------------------------------------------- //============================================================================= // HPE_BEGIN // [dwenger] Steam Cloud Support //============================================================================= CAchievementMgr::CAchievementMgr( SteamCloudPersisting ePersistToSteamCloud ) : CAutoGameSystemPerFrame( "CAchievementMgr" ) //============================================================================= // HPE_END //============================================================================= #if !defined(NO_STEAM) , m_CallbackUserStatsReceived( this, &CAchievementMgr::Steam_OnUserStatsReceived ), m_CallbackUserStatsStored( this, &CAchievementMgr::Steam_OnUserStatsStored ) #endif { SetDefLessFunc( m_mapAchievement ); SetDefLessFunc( m_mapMetaAchievement ); m_flLastClassChangeTime = 0; m_flTeamplayStartTime = 0; m_iMiniroundsCompleted = 0; m_szMap[0] = 0; m_bSteamDataDirty = false; m_bGlobalStateDirty = false; m_bGlobalStateLoaded = false; m_bCheatsEverOn = false; m_flTimeLastSaved = 0; //============================================================================= // HPE_BEGIN // [dwenger] Steam Cloud Support //============================================================================= if ( ePersistToSteamCloud == SteamCloudPersist_Off ) { m_bPersistToSteamCloud = false; } else { m_bPersistToSteamCloud = true; } //============================================================================= // HPE_END //============================================================================= m_AchievementsAwarded.RemoveAll(); } //----------------------------------------------------------------------------- // Purpose: Initializer //----------------------------------------------------------------------------- bool CAchievementMgr::Init() { // We can be created on either client (for multiplayer games) or server // (for single player), so register ourselves with the engine so UI has a uniform place // to go get the pointer to us #ifdef _DEBUG // There can be only one achievement manager instance; no one else should be registered IAchievementMgr *pAchievementMgr = engine->GetAchievementMgr(); Assert( NULL == pAchievementMgr ); #endif // _DEBUG // register ourselves engine->SetAchievementMgr( this ); // register for events #ifdef GAME_DLL ListenForGameEvent( "entity_killed" ); ListenForGameEvent( "game_init" ); #else ListenForGameEvent( "player_death" ); ListenForGameEvent( "player_stats_updated" ); usermessages->HookMessage( "AchievementEvent", MsgFunc_AchievementEvent ); #endif // CLIENT_DLL #ifdef TF_CLIENT_DLL ListenForGameEvent( "localplayer_changeclass" ); ListenForGameEvent( "localplayer_changeteam" ); ListenForGameEvent( "teamplay_round_start" ); ListenForGameEvent( "teamplay_round_win" ); #endif // TF_CLIENT_DLL return true; } //----------------------------------------------------------------------------- // Purpose: called at init time after all systems are init'd. We have to // do this in PostInit because the Steam app ID is not available earlier //----------------------------------------------------------------------------- void CAchievementMgr::PostInit() { if ( !g_AchievementSaveThread.IsAlive() ) { g_AchievementSaveThread.Start(); #ifdef WIN32 if ( IsX360() ) { ThreadSetAffinity( (ThreadHandle_t)g_AchievementSaveThread.GetThreadHandle(), XBOX_PROCESSOR_3 ); } #endif // WIN32 } // get current game dir const char *pGameDir = COM_GetModDirectory(); CBaseAchievementHelper *pAchievementHelper = CBaseAchievementHelper::s_pFirst; while ( pAchievementHelper ) { // create and initialize all achievements and insert them in our map CBaseAchievement *pAchievement = pAchievementHelper->m_pfnCreate(); pAchievement->m_pAchievementMgr = this; pAchievement->Init(); pAchievement->CalcProgressMsgIncrement(); // only add an achievement if it does not have a game filter (only compiled into the game it // applies to, or truly cross-game) or, if it does have a game filter, the filter matches current game. // (e.g. EP 1/2/... achievements are in shared binary but are game specific, they have a game filter for runtime check.) const char *pGameDirFilter = pAchievement->m_pGameDirFilter; if ( !pGameDirFilter || ( 0 == Q_strcmp( pGameDir, pGameDirFilter ) ) ) { m_mapAchievement.Insert( pAchievement->GetAchievementID(), pAchievement ); if ( pAchievement->IsMetaAchievement() ) { m_mapMetaAchievement.Insert( pAchievement->GetAchievementID(), dynamic_cast(pAchievement) ); } } else { // achievement is not for this game, don't use it delete pAchievement; } pAchievementHelper = pAchievementHelper->m_pNext; } FOR_EACH_MAP( m_mapAchievement, iter ) { m_vecAchievement.AddToTail( m_mapAchievement[iter] ); } // load global state from file LoadGlobalState(); // download achievements/stats from Steam/XBox Live DownloadUserData(); } //----------------------------------------------------------------------------- // Purpose: Shuts down the achievement manager //----------------------------------------------------------------------------- void CAchievementMgr::Shutdown() { g_AchievementSaveThread.CallWorker( CAchievementSaveThread::EXIT ); SaveGlobalState( false ); // we just told the thread to shutdown so don't try an async save here FOR_EACH_MAP( m_mapAchievement, iter ) { delete m_mapAchievement[iter]; } m_mapAchievement.RemoveAll(); m_mapMetaAchievement.RemoveAll(); m_vecAchievement.RemoveAll(); m_vecKillEventListeners.RemoveAll(); m_vecMapEventListeners.RemoveAll(); m_vecComponentListeners.RemoveAll(); m_AchievementsAwarded.RemoveAll(); m_bGlobalStateLoaded = false; } //----------------------------------------------------------------------------- // Purpose: Cleans up all achievements and then re-initializes them //----------------------------------------------------------------------------- void CAchievementMgr::InitializeAchievements() { Shutdown(); PostInit(); } #ifdef CLIENT_DLL extern const ConVar *sv_cheats; #endif #ifdef GAME_DLL void CAchievementMgr::FrameUpdatePostEntityThink() { Update( 0.0f ); } #endif //----------------------------------------------------------------------------- // Purpose: Do per-frame handling //----------------------------------------------------------------------------- void CAchievementMgr::Update( float frametime ) { #ifdef CLIENT_DLL if ( !sv_cheats ) { sv_cheats = cvar->FindVar( "sv_cheats" ); } #endif #ifndef _DEBUG // keep track if cheats have ever been turned on during this level if ( !WereCheatsEverOn() ) { if ( sv_cheats && sv_cheats->GetBool() ) { m_bCheatsEverOn = true; } } #endif // Call think functions. Work backwards, because we may remove achievements from the list. int iCount = m_vecThinkListeners.Count(); for ( int i = iCount-1; i >= 0; i-- ) { if ( m_vecThinkListeners[i].m_flThinkTime < gpGlobals->curtime ) { m_vecThinkListeners[i].pAchievement->Think(); // The think function may have pushed out the think time. If not, remove ourselves from the list. if ( m_vecThinkListeners[i].pAchievement->IsAchieved() || m_vecThinkListeners[i].m_flThinkTime < gpGlobals->curtime ) { m_vecThinkListeners.Remove(i); } } } if ( m_bSteamDataDirty ) { UploadUserData(); } } //----------------------------------------------------------------------------- // Purpose: called on level init //----------------------------------------------------------------------------- void CAchievementMgr::LevelInitPreEntity() { m_bCheatsEverOn = false; // load global state if we haven't already; X360 users may not have had a storage device available or selected at boot time EnsureGlobalStateLoaded(); #ifdef GAME_DLL // For single-player games, achievement mgr must live on the server. (Only the server has detailed knowledge of game state.) Assert( !GameRules()->IsMultiplayer() ); #else // For multiplayer games, achievement mgr must live on the client. (Only the client can read/write player state from Steam/XBox Live.) Assert( GameRules()->IsMultiplayer() ); #endif // clear list of achievements listening for events m_vecKillEventListeners.RemoveAll(); m_vecMapEventListeners.RemoveAll(); m_vecComponentListeners.RemoveAll(); m_AchievementsAwarded.RemoveAll(); m_flLastClassChangeTime = 0; m_flTeamplayStartTime = 0; m_iMiniroundsCompleted = 0; // client and server have map names available in different forms (full path on client, just file base name on server), // cache it in base file name form here so we don't have to have different code paths each time we access it #ifdef CLIENT_DLL Q_FileBase( engine->GetLevelName(), m_szMap, ARRAYSIZE( m_szMap ) ); #else Q_strncpy( m_szMap, gpGlobals->mapname.ToCStr(), ARRAYSIZE( m_szMap ) ); #endif // CLIENT_DLL if ( IsX360() ) { // need to remove the .360 extension on the end of the map name char *pExt = Q_stristr( m_szMap, ".360" ); if ( pExt ) { *pExt = '\0'; } } // look through all achievements, see which ones we want to have listen for events FOR_EACH_MAP( m_mapAchievement, iAchievement ) { CBaseAchievement *pAchievement = m_mapAchievement[iAchievement]; // if the achievement only applies to a specific map, and it's not the current map, skip it const char *pMapNameFilter = pAchievement->m_pMapNameFilter; if ( pMapNameFilter && ( 0 != Q_strcmp( m_szMap, pMapNameFilter ) ) ) continue; // if the achievement needs kill events, add it as a listener if ( pAchievement->GetFlags() & ACH_LISTEN_KILL_EVENTS ) { m_vecKillEventListeners.AddToTail( pAchievement ); } // if the achievement needs map events, add it as a listener if ( pAchievement->GetFlags() & ACH_LISTEN_MAP_EVENTS ) { m_vecMapEventListeners.AddToTail( pAchievement ); } // if the achievement needs map events, add it as a listener if ( pAchievement->GetFlags() & ACH_LISTEN_COMPONENT_EVENTS ) { m_vecComponentListeners.AddToTail( pAchievement ); } if ( pAchievement->IsActive() ) { pAchievement->ListenForEvents(); } } m_flLevelInitTime = gpGlobals->curtime; } //----------------------------------------------------------------------------- // Purpose: called on level shutdown //----------------------------------------------------------------------------- void CAchievementMgr::LevelShutdownPreEntity() { // make all achievements stop listening for events FOR_EACH_MAP( m_mapAchievement, iAchievement ) { CBaseAchievement *pAchievement = m_mapAchievement[iAchievement]; if ( !pAchievement->AlwaysListen() ) { pAchievement->StopListeningForAllEvents(); } } // save global state if we have any changes SaveGlobalStateIfDirty(); UploadUserData(); } //----------------------------------------------------------------------------- // Purpose: returns achievement for specified ID //----------------------------------------------------------------------------- CBaseAchievement *CAchievementMgr::GetAchievementByID( int iAchievementID ) { int iAchievement = m_mapAchievement.Find( iAchievementID ); if ( iAchievement != m_mapAchievement.InvalidIndex() ) { return m_mapAchievement[iAchievement]; } return NULL; } //----------------------------------------------------------------------------- // Purpose: returns achievement with specified name. NOTE: this iterates through // all achievements to find the name, intended for debugging purposes. // Use GetAchievementByID for fast lookup. //----------------------------------------------------------------------------- CBaseAchievement *CAchievementMgr::GetAchievementByName( const char *pchName ) { VPROF("GetAchievementByName"); FOR_EACH_MAP_FAST( m_mapAchievement, i ) { CBaseAchievement *pAchievement = m_mapAchievement[i]; if ( pAchievement && 0 == ( Q_stricmp( pchName, pAchievement->GetName() ) ) ) return pAchievement; } return NULL; } //----------------------------------------------------------------------------- // Purpose: Returns true if the achievement with the specified name has been achieved //----------------------------------------------------------------------------- bool CAchievementMgr::HasAchieved( const char *pchName ) { CBaseAchievement *pAchievement = GetAchievementByName( pchName ); if ( pAchievement ) return pAchievement->IsAchieved(); return false; } //----------------------------------------------------------------------------- // Purpose: downloads user data from Steam or XBox Live //----------------------------------------------------------------------------- void CAchievementMgr::DownloadUserData() { if ( IsPC() ) { #ifndef NO_STEAM if ( steamapicontext->SteamUserStats() ) { // request stat download; will get called back at OnUserStatsReceived when complete steamapicontext->SteamUserStats()->RequestCurrentStats(); } #endif } else if ( IsX360() ) { #if defined( _X360 ) if ( XBX_GetPrimaryUserId() == INVALID_USER_ID ) return; // Download achievements from XBox Live bool bDownloadSuccessful = true; int nTotalAchievements = 99; uint bytes; int ret = xboxsystem->EnumerateAchievements( XBX_GetPrimaryUserId(), 0, 0, nTotalAchievements, &bytes, 0, false ); if ( ret != ERROR_SUCCESS ) { Warning( "Enumerate Achievements failed! Error %d", ret ); bDownloadSuccessful = false; } // Enumerate the achievements from Live void *pBuffer = new byte[bytes]; if ( bDownloadSuccessful ) { ret = xboxsystem->EnumerateAchievements( XBX_GetPrimaryUserId(), 0, 0, nTotalAchievements, pBuffer, bytes, false ); if ( ret != nTotalAchievements ) { Warning( "Enumerate Achievements failed! Error %d", ret ); bDownloadSuccessful = false; } } if ( bDownloadSuccessful ) { // Give live a chance to mark achievements as unlocked, in case the achievement manager // wasn't able to get that data (storage device missing, read failure, etc) XACHIEVEMENT_DETAILS *pXboxAchievements = (XACHIEVEMENT_DETAILS*)pBuffer; for ( int i = 0; i < nTotalAchievements; ++i ) { CBaseAchievement *pAchievement = GetAchievementByID( pXboxAchievements[i].dwId ); if ( !pAchievement ) continue; // Give Live a chance to claim the achievement as unlocked if ( AchievementEarned( pXboxAchievements[i].dwFlags ) ) { pAchievement->SetAchieved( true ); } } } delete pBuffer; #endif // X360 } } const char *COM_GetModDirectory() { static char modDir[MAX_PATH]; if ( Q_strlen( modDir ) == 0 ) { const char *gamedir = CommandLine()->ParmValue("-game", CommandLine()->ParmValue( "-defaultgamedir", "hl2" ) ); Q_strncpy( modDir, gamedir, sizeof(modDir) ); if ( strchr( modDir, '/' ) || strchr( modDir, '\\' ) ) { Q_StripLastDir( modDir, sizeof(modDir) ); int dirlen = Q_strlen( modDir ); Q_strncpy( modDir, gamedir + dirlen, sizeof(modDir) - dirlen ); } } return modDir; } //----------------------------------------------------------------------------- // Purpose: uploads user data to steam //----------------------------------------------------------------------------- void CAchievementMgr::UploadUserData() { if ( IsPC() ) { #ifndef NO_STEAM if ( steamapicontext->SteamUserStats() ) { // Upload current Steam client achievements & stats state to Steam. Will get called back at OnUserStatsStored when complete. // Only values previously set via SteamUserStats() get uploaded steamapicontext->SteamUserStats()->StoreStats(); m_bSteamDataDirty = false; } #endif } } //----------------------------------------------------------------------------- // Purpose: loads global state from file //----------------------------------------------------------------------------- void CAchievementMgr::LoadGlobalState() { if ( IsX360() ) { #ifdef _X360 if ( XBX_GetStorageDeviceId() == XBX_INVALID_STORAGE_ID || XBX_GetStorageDeviceId() == XBX_STORAGE_DECLINED ) return; #endif } char szFilename[_MAX_PATH]; if ( IsX360() ) { Q_snprintf( szFilename, sizeof( szFilename ), "cfg:/%s_GameState.txt", COM_GetModDirectory() ); } else { Q_snprintf( szFilename, sizeof( szFilename ), "GameState.txt" ); } //============================================================================= // HPE_BEGIN // [dwenger] Steam Cloud Support //============================================================================= if ( m_bPersistToSteamCloud ) { #ifndef NO_STEAM ISteamRemoteStorage *pRemoteStorage = SteamClient()?(ISteamRemoteStorage *)SteamClient()->GetISteamGenericInterface( SteamAPI_GetHSteamUser(), SteamAPI_GetHSteamPipe(), STEAMREMOTESTORAGE_INTERFACE_VERSION ):NULL; if (pRemoteStorage) { if (pRemoteStorage->FileExists(szFilename)) { int32 fileSize = pRemoteStorage->GetFileSize(szFilename); if (fileSize > 0) { // Allocate space for the file data char* pData = new char[fileSize]; if (pData) { int32 sizeRead = pRemoteStorage->FileRead(szFilename, pData, fileSize); if (sizeRead == fileSize) { // Write out data to a filesystem GameState file that can be read by the original code below FileHandle_t handle = filesystem->Open(szFilename, "w"); if (handle) { filesystem->Write(pData, fileSize, handle); filesystem->Close(handle); } } // Delete the data array delete []pData; } } } } #endif } //============================================================================= // HPE_END //============================================================================= KeyValues *pKV = new KeyValues("GameState" ); if ( pKV->LoadFromFile( filesystem, szFilename, "MOD" ) ) { KeyValues *pNode = pKV->GetFirstSubKey(); while ( pNode ) { // look up this achievement int iAchievementID = pNode->GetInt( "id", 0 ); if ( iAchievementID > 0 ) { CBaseAchievement *pAchievement = GetAchievementByID( iAchievementID ); if ( pAchievement ) { pAchievement->ApplySettings(pNode); } } pNode = pNode->GetNextKey(); } m_bGlobalStateLoaded = true; } } //----------------------------------------------------------------------------- // Purpose: saves global state to file //----------------------------------------------------------------------------- void CAchievementMgr::SaveGlobalState( bool bAsync ) { VPROF_BUDGET( "CAchievementMgr::SaveGlobalState", "Achievements" ); KeyValues *pKV = new KeyValues("GameState" ); FOR_EACH_MAP( m_mapAchievement, i ) { CBaseAchievement *pAchievement = m_mapAchievement[i]; if ( pAchievement->ShouldSaveGlobal() ) { KeyValues *pNode = pKV->CreateNewKey(); pNode->SetInt( "id", pAchievement->GetAchievementID() ); pAchievement->GetSettings(pNode); } } if ( !bAsync ) { WriteAchievementGlobalState( pKV, m_bPersistToSteamCloud ); } else { g_AchievementSaveThread.WriteAchievementGlobalState( pKV ); } m_flTimeLastSaved = Plat_FloatTime(); m_bGlobalStateDirty = false; } //----------------------------------------------------------------------------- // Purpose: loads global state if we have not already successfully loaded it //----------------------------------------------------------------------------- void CAchievementMgr::EnsureGlobalStateLoaded() { if ( !m_bGlobalStateLoaded ) { LoadGlobalState(); } } //----------------------------------------------------------------------------- // Purpose: saves global state to file if there have been any changes //----------------------------------------------------------------------------- void CAchievementMgr::SaveGlobalStateIfDirty( bool bAsync ) { if ( m_bGlobalStateDirty ) { SaveGlobalState( bAsync ); } } //----------------------------------------------------------------------------- // Purpose: awards specified achievement //----------------------------------------------------------------------------- void CAchievementMgr::AwardAchievement( int iAchievementID ) { CBaseAchievement *pAchievement = GetAchievementByID( iAchievementID ); Assert( pAchievement ); if ( !pAchievement ) return; if ( !pAchievement->AlwaysEnabled() && !CheckAchievementsEnabled() ) { Msg( "Achievements disabled, ignoring achievement unlock for %s\n", pAchievement->GetName() ); return; } if ( pAchievement->IsAchieved() ) { if ( cc_achievement_debug.GetInt() > 0 ) { Msg( "Achievement award called but already achieved: %s\n", pAchievement->GetName() ); } return; } pAchievement->SetAchieved( true ); #ifdef CLIENT_DLL if ( gamestats ) { gamestats->Event_AchievementProgress( pAchievement->GetAchievementID(), pAchievement->GetName() ); } #endif //============================================================================= // HPE_BEGIN //============================================================================= // [dwenger] Necessary for sorting achievements by award time pAchievement->OnAchieved(); // [tj] IGameEvent * event = gameeventmanager->CreateEvent( "achievement_earned_local" ); if ( event ) { event->SetInt( "achievement", pAchievement->GetAchievementID() ); gameeventmanager->FireEventClientSide( event ); } //============================================================================= // HPE_END //============================================================================= if ( cc_achievement_debug.GetInt() > 0 ) { Msg( "Achievement awarded: %s\n", pAchievement->GetName() ); } // save state at next good opportunity. (Don't do it immediately, may hitch at bad time.) SetDirty( true ); if ( IsPC() ) { #ifndef NO_STEAM if ( steamapicontext->SteamUserStats() ) { VPROF_BUDGET( "AwardAchievement", VPROF_BUDGETGROUP_STEAM ); // set this achieved in the Steam client bool bRet = steamapicontext->SteamUserStats()->SetAchievement( pAchievement->GetName() ); // Assert( bRet ); if ( bRet ) { m_AchievementsAwarded.AddToTail( iAchievementID ); } } #endif } else if ( IsX360() ) { #ifdef _X360 if ( xboxsystem ) xboxsystem->AwardAchievement( XBX_GetPrimaryUserId(), iAchievementID ); #endif } } //----------------------------------------------------------------------------- // Purpose: updates specified achievement //----------------------------------------------------------------------------- void CAchievementMgr::UpdateAchievement( int iAchievementID, int nData ) { CBaseAchievement *pAchievement = GetAchievementByID( iAchievementID ); Assert( pAchievement ); if ( !pAchievement ) return; if ( !pAchievement->AlwaysEnabled() && !CheckAchievementsEnabled() ) { Msg( "Achievements disabled, ignoring achievement update for %s\n", pAchievement->GetName() ); return; } if ( pAchievement->IsAchieved() ) { if ( cc_achievement_debug.GetInt() > 0 ) { Msg( "Achievement update called but already achieved: %s\n", pAchievement->GetName() ); } return; } pAchievement->UpdateAchievement( nData ); } //----------------------------------------------------------------------------- // Purpose: clears state for all achievements //----------------------------------------------------------------------------- void CAchievementMgr::PreRestoreSavedGame() { // load global state if we haven't already; X360 users may not have had a storage device available or selected at boot time EnsureGlobalStateLoaded(); FOR_EACH_MAP( m_mapAchievement, i ) { m_mapAchievement[i]->PreRestoreSavedGame(); } } //----------------------------------------------------------------------------- // Purpose: clears state for all achievements //----------------------------------------------------------------------------- void CAchievementMgr::PostRestoreSavedGame() { FOR_EACH_MAP( m_mapAchievement, i ) { m_mapAchievement[i]->PostRestoreSavedGame(); } } extern bool IsInCommentaryMode( void ); //----------------------------------------------------------------------------- // Purpose: checks if achievements are enabled //----------------------------------------------------------------------------- bool CAchievementMgr::CheckAchievementsEnabled() { // if PC, Steam must be running and user logged in if ( IsPC() && !LoggedIntoSteam() ) { Msg( "Achievements disabled: Steam not running.\n" ); return false; } #if defined( _X360 ) uint state = XUserGetSigninState( XBX_GetPrimaryUserId() ); if ( state == eXUserSigninState_NotSignedIn ) { Msg( "Achievements disabled: not signed in to XBox user account.\n" ); return false; } #endif // can't be in commentary mode, user is invincible if ( IsInCommentaryMode() ) { Msg( "Achievements disabled: in commentary mode.\n" ); return false; } #ifdef CLIENT_DLL // achievements disabled if playing demo if ( engine->IsPlayingDemo() ) { Msg( "Achievements disabled: demo playing.\n" ); return false; } #endif // CLIENT_DLL #ifdef CSTRIKE_DLL //============================================================================= // HPE_BEGIN: // [Forrest] Allow achievements/stats to be turned off for a server //============================================================================= if ( sv_nostats.GetBool() ) { // prevent message spam const float fNotificationCooldown = 60.0f; static float fNextNotification = 0.0f; if (gpGlobals->curtime >= fNextNotification) { Msg( "Achievements and stats disabled: sv_nostats is set.\n" ); fNextNotification = gpGlobals->curtime + fNotificationCooldown; } return false; } //============================================================================= // HPE_END //============================================================================= #endif // CSTRIKE_DLL #if defined(TF_DLL) || defined(TF_CLIENT_DLL) // no achievements for now in training if ( TFGameRules() && TFGameRules()->IsInTraining() && TFGameRules()->AllowTrainingAchievements() == false ) { return false; } ConVarRef tf_bot_offline_practice( "tf_bot_offline_practice" ); // no achievements for offline practice if ( tf_bot_offline_practice.GetInt() != 0 ) { return false; } #endif #if DEBUG_ACHIEVEMENTS_IN_RELEASE return true; #endif if ( IsPC() ) { // Don't award achievements if cheats are turned on. if ( WereCheatsEverOn() ) { #ifndef NO_STEAM // Cheats get turned on automatically if you run with -dev which many people do internally, so allow cheats if developer is turned on and we're not running // on Steam public if ( developer.GetInt() == 0 || ( k_EUniverseInvalid == GetUniverse() ) || ( k_EUniversePublic == GetUniverse() ) ) { Msg( "Achievements disabled: cheats turned on in this app session.\n" ); return false; } #endif } } return true; } #ifdef CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: Returns the whether all of the local player's team mates are // on her friends list, and if there are at least the specified # of // teammates. Involves cross-process calls to Steam so this is mildly // expensive, don't call this every frame. //----------------------------------------------------------------------------- bool CalcPlayersOnFriendsList( int iMinFriends ) { // Got message during connection if ( !g_PR ) return false; Assert( g_pGameRules->IsMultiplayer() ); // Do a cheap rejection: check teammate count first to see if we even need to bother checking w/Steam // Subtract 1 for the local player. if ( CalcPlayerCount()-1 < iMinFriends ) return false; // determine local player team int iLocalPlayerIndex = GetLocalPlayerIndex(); uint64 XPlayerUid = 0; if ( IsPC() ) { #ifndef NO_STEAM if ( !steamapicontext->SteamFriends() || !steamapicontext->SteamUtils() || !g_pGameRules->IsMultiplayer() ) #endif return false; } else if ( IsX360() ) { if ( !matchmaking ) return false; XPlayerUid = XBX_GetPrimaryUserId(); } else { // other platforms...? return false; } // Loop through the players int iTotalFriends = 0; for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) { // find all players who are on the local player's team if( ( iPlayerIndex != iLocalPlayerIndex ) && ( g_PR->IsConnected( iPlayerIndex ) ) ) { if ( IsPC() ) { player_info_t pi; if ( !engine->GetPlayerInfo( iPlayerIndex, &pi ) ) continue; if ( !pi.friendsID ) continue; #ifndef NO_STEAM // check and see if they're on the local player's friends list CSteamID steamID( pi.friendsID, 1, GetUniverse(), k_EAccountTypeIndividual ); if ( !steamapicontext->SteamFriends()->HasFriend( steamID, /*k_EFriendFlagImmediate*/ 0x04 ) ) continue; #endif } else if ( IsX360() ) { uint64 XUid[1]; XUid[0] = matchmaking->PlayerIdToXuid( iPlayerIndex ); BOOL bFriend; #ifdef _X360 XUserAreUsersFriends( XPlayerUid, XUid, 1, &bFriend, NULL ); #endif // _X360 if ( !bFriend ) continue; } iTotalFriends++; } } return (iTotalFriends >= iMinFriends); } //----------------------------------------------------------------------------- // Purpose: Returns whether there are a specified # of teammates who all belong // to same clan as local player. Involves cross-process calls to Steam // so this is mildly expensive, don't call this every frame. //----------------------------------------------------------------------------- bool CalcHasNumClanPlayers( int iClanTeammates ) { Assert( g_pGameRules->IsMultiplayer() ); if ( IsPC() ) { #ifndef _X360 // Do a cheap rejection: check teammate count first to see if we even need to bother checking w/Steam // Subtract 1 for the local player. if ( CalcPlayerCount()-1 < iClanTeammates ) return false; if ( !steamapicontext->SteamFriends() || !steamapicontext->SteamUtils() || !g_pGameRules->IsMultiplayer() ) return false; // determine local player team int iLocalPlayerIndex = GetLocalPlayerIndex(); for ( int iClan = 0; iClan < steamapicontext->SteamFriends()->GetClanCount(); iClan++ ) { int iClanMembersOnTeam = 0; CSteamID clanID = steamapicontext->SteamFriends()->GetClanByIndex( iClan ); // enumerate all players for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) { if( ( iPlayerIndex != iLocalPlayerIndex ) && ( g_PR->IsConnected( iPlayerIndex ) ) ) { player_info_t pi; if ( engine->GetPlayerInfo( iPlayerIndex, &pi ) && ( pi.friendsID ) ) { // check and see if they're on the local player's friends list CSteamID steamID( pi.friendsID, 1, GetUniverse(), k_EAccountTypeIndividual ); if ( steamapicontext->SteamFriends()->IsUserInSource( steamID, clanID ) ) { iClanMembersOnTeam++; if ( iClanMembersOnTeam == iClanTeammates ) return true; } } } } } #endif return false; } else if ( IsX360() ) { // TODO: implement for 360 return false; } else { // other platforms...? return false; } } //----------------------------------------------------------------------------- // Purpose: Returns the # of teammates of the local player //----------------------------------------------------------------------------- int CalcTeammateCount() { Assert( g_pGameRules->IsMultiplayer() ); // determine local player team int iLocalPlayerIndex = GetLocalPlayerIndex(); int iLocalPlayerTeam = g_PR->GetTeam( iLocalPlayerIndex ); int iNumTeammates = 0; for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) { // find all players who are on the local player's team if( ( iPlayerIndex != iLocalPlayerIndex ) && ( g_PR->IsConnected( iPlayerIndex ) ) && ( g_PR->GetTeam( iPlayerIndex ) == iLocalPlayerTeam ) ) { iNumTeammates++; } } return iNumTeammates; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CalcPlayerCount() { int iCount = 0; for( int iPlayerIndex = 1 ; iPlayerIndex <= MAX_PLAYERS; iPlayerIndex++ ) { // find all players who are on the local player's team if( g_PR->IsConnected( iPlayerIndex ) ) { iCount++; } } return iCount; } #endif // CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: Resets all achievements. For debugging purposes only //----------------------------------------------------------------------------- void CAchievementMgr::ResetAchievements() { if ( !IsPC() ) { DevMsg( "Only available on PC\n" ); return; } if ( !LoggedIntoSteam() ) { Msg( "Steam not running, achievements disabled. Cannot reset achievements.\n" ); return; } FOR_EACH_MAP( m_mapAchievement, i ) { CBaseAchievement *pAchievement = m_mapAchievement[i]; ResetAchievement_Internal( pAchievement ); } #ifndef NO_STEAM if ( steamapicontext->SteamUserStats() ) { steamapicontext->SteamUserStats()->StoreStats(); } #endif if ( cc_achievement_debug.GetInt() > 0 ) { Msg( "All achievements reset.\n" ); } } void CAchievementMgr::ResetAchievement( int iAchievementID ) { if ( !IsPC() ) { DevMsg( "Only available on PC\n" ); return; } if ( !LoggedIntoSteam() ) { Msg( "Steam not running, achievements disabled. Cannot reset achievements.\n" ); return; } CBaseAchievement *pAchievement = GetAchievementByID( iAchievementID ); Assert( pAchievement ); if ( pAchievement ) { ResetAchievement_Internal( pAchievement ); #ifndef NO_STEAM if ( steamapicontext->SteamUserStats() ) { steamapicontext->SteamUserStats()->StoreStats(); } #endif if ( cc_achievement_debug.GetInt() > 0 ) { Msg( "Achievement %s reset.\n", pAchievement->GetName() ); } } } //----------------------------------------------------------------------------- // Purpose: Resets all achievements. For debugging purposes only //----------------------------------------------------------------------------- void CAchievementMgr::PrintAchievementStatus() { if ( IsPC() && !LoggedIntoSteam() ) { Msg( "Steam not running, achievements disabled. Cannot view or unlock achievements.\n" ); return; } Msg( "%42s %-20s %s\n", "Name:", "Status:", "Point value:" ); int iTotalAchievements = 0, iTotalPoints = 0; FOR_EACH_MAP( m_mapAchievement, i ) { CBaseAchievement *pAchievement = m_mapAchievement[i]; Msg( "%42s ", pAchievement->GetName() ); CFailableAchievement *pFailableAchievement = dynamic_cast( pAchievement ); if ( pAchievement->IsAchieved() ) { Msg( "%-20s", "ACHIEVED" ); } else if ( pFailableAchievement && pFailableAchievement->IsFailed() ) { Msg( "%-20s", "FAILED" ); } else { char szBuf[255]; Q_snprintf( szBuf, ARRAYSIZE( szBuf ), "(%d/%d)%s", pAchievement->GetCount(), pAchievement->GetGoal(), pAchievement->IsActive() ? "" : " (inactive)" ); Msg( "%-20s", szBuf ); } Msg( " %d ", pAchievement->GetPointValue() ); pAchievement->PrintAdditionalStatus(); Msg( "\n" ); iTotalAchievements++; iTotalPoints += pAchievement->GetPointValue(); } Msg( "Total achievements: %d Total possible points: %d\n", iTotalAchievements, iTotalPoints ); } //----------------------------------------------------------------------------- // Purpose: called when a game event is fired //----------------------------------------------------------------------------- void CAchievementMgr::FireGameEvent( IGameEvent *event ) { VPROF_( "CAchievementMgr::FireGameEvent", 1, VPROF_BUDGETGROUP_STEAM, false, 0 ); const char *name = event->GetName(); if ( name == NULL ) { return; } if ( 0 == Q_strcmp( name, "entity_killed" ) ) { #ifdef GAME_DLL CBaseEntity *pVictim = UTIL_EntityByIndex( event->GetInt( "entindex_killed", 0 ) ); CBaseEntity *pAttacker = UTIL_EntityByIndex( event->GetInt( "entindex_attacker", 0 ) ); CBaseEntity *pInflictor = UTIL_EntityByIndex( event->GetInt( "entindex_inflictor", 0 ) ); OnKillEvent( pVictim, pAttacker, pInflictor, event ); #endif // GAME_DLL } else if ( 0 == Q_strcmp( name, "game_init" ) ) { #ifdef GAME_DLL // clear all state as though we were loading a saved game, but without loading the game PreRestoreSavedGame(); PostRestoreSavedGame(); #endif // GAME_DLL } #ifdef CLIENT_DLL else if ( 0 == Q_strcmp( name, "player_death" ) ) { CBaseEntity *pVictim = ClientEntityList().GetEnt( engine->GetPlayerForUserID( event->GetInt("userid") ) ); CBaseEntity *pAttacker = ClientEntityList().GetEnt( engine->GetPlayerForUserID( event->GetInt("attacker") ) ); OnKillEvent( pVictim, pAttacker, NULL, event ); } else if ( 0 == Q_strcmp( name, "localplayer_changeclass" ) ) { // keep track of when the player last changed class m_flLastClassChangeTime = gpGlobals->curtime; } else if ( 0 == Q_strcmp( name, "localplayer_changeteam" ) ) { // keep track of the time of transitions to and from a game team C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer(); if ( pLocalPlayer ) { int iTeam = pLocalPlayer->GetTeamNumber(); if ( iTeam > TEAM_SPECTATOR ) { if ( 0 == m_flTeamplayStartTime ) { // player transitioned from no/spectator team to a game team, mark the time m_flTeamplayStartTime = gpGlobals->curtime; } } else { // player transitioned to no/spectator team, clear the teamplay start time m_flTeamplayStartTime = 0; } } } else if ( 0 == Q_strcmp( name, "teamplay_round_start" ) ) { if ( event->GetBool( "full_reset" ) ) { // we're starting a full round, clear miniround count m_iMiniroundsCompleted = 0; } } else if ( 0 == Q_strcmp( name, "teamplay_round_win" ) ) { if ( false == event->GetBool( "full_round", true ) ) { // we just finished a miniround but the round is continuing, increment miniround count m_iMiniroundsCompleted ++; } } else if ( 0 == Q_strcmp( name, "player_stats_updated" ) ) { FOR_EACH_MAP( m_mapAchievement, i ) { CBaseAchievement *pAchievement = m_mapAchievement[i]; pAchievement->OnPlayerStatsUpdate(); } } #endif // CLIENT_DLL } //----------------------------------------------------------------------------- // Purpose: called when a player or character has been killed //----------------------------------------------------------------------------- void CAchievementMgr::OnKillEvent( CBaseEntity *pVictim, CBaseEntity *pAttacker, CBaseEntity *pInflictor, IGameEvent *event ) { // can have a NULL victim on client if victim has never entered local player's PVS if ( !pVictim ) return; // if single-player game, calculate if the attacker is the local player and if the victim is the player enemy bool bAttackerIsPlayer = false; bool bVictimIsPlayerEnemy = false; #ifdef GAME_DLL if ( !g_pGameRules->IsMultiplayer() ) { CBasePlayer *pLocalPlayer = UTIL_GetLocalPlayer(); if ( pLocalPlayer ) { if ( pAttacker == pLocalPlayer ) { bAttackerIsPlayer = true; } CBaseCombatCharacter *pBCC = dynamic_cast( pVictim ); if ( pBCC && ( D_HT == pBCC->IRelationType( pLocalPlayer ) ) ) { bVictimIsPlayerEnemy = true; } } } #else C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer(); bVictimIsPlayerEnemy = !pLocalPlayer->InSameTeam( pVictim ); if ( pAttacker == pLocalPlayer ) { bAttackerIsPlayer = true; } #endif // GAME_DLL // look through all the kill event listeners and notify any achievements whose filters we pass FOR_EACH_VEC( m_vecKillEventListeners, iAchievement ) { CBaseAchievement *pAchievement = m_vecKillEventListeners[iAchievement]; if ( !pAchievement->IsActive() ) continue; #ifdef CLIENT_DLL // Swallow kill events that can't be earned right now if ( !pAchievement->LocalPlayerCanEarn() ) continue; #endif // if this achievement only looks for kills where attacker is player and that is not the case here, skip this achievement if ( ( pAchievement->GetFlags() & ACH_FILTER_ATTACKER_IS_PLAYER ) && !bAttackerIsPlayer ) continue; // if this achievement only looks for kills where victim is killer enemy and that is not the case here, skip this achievement if ( ( pAchievement->GetFlags() & ACH_FILTER_VICTIM_IS_PLAYER_ENEMY ) && !bVictimIsPlayerEnemy ) continue; #if GAME_DLL // if this achievement only looks for a particular victim class name and this victim is a different class, skip this achievement const char *pVictimClassNameFilter = pAchievement->m_pVictimClassNameFilter; if ( pVictimClassNameFilter && !pVictim->ClassMatches( pVictimClassNameFilter ) ) continue; // if this achievement only looks for a particular inflictor class name and this inflictor is a different class, skip this achievement const char *pInflictorClassNameFilter = pAchievement->m_pInflictorClassNameFilter; if ( pInflictorClassNameFilter && ( ( NULL == pInflictor ) || !pInflictor->ClassMatches( pInflictorClassNameFilter ) ) ) continue; // if this achievement only looks for a particular attacker class name and this attacker is a different class, skip this achievement const char *pAttackerClassNameFilter = pAchievement->m_pAttackerClassNameFilter; if ( pAttackerClassNameFilter && ( ( NULL == pAttacker ) || !pAttacker->ClassMatches( pAttackerClassNameFilter ) ) ) continue; // if this achievement only looks for a particular inflictor entity name and this inflictor has a different name, skip this achievement const char *pInflictorEntityNameFilter = pAchievement->m_pInflictorEntityNameFilter; if ( pInflictorEntityNameFilter && ( ( NULL == pInflictor ) || !pInflictor->NameMatches( pInflictorEntityNameFilter ) ) ) continue; #endif // GAME_DLL // we pass all filters for this achievement, notify the achievement of the kill pAchievement->Event_EntityKilled( pVictim, pAttacker, pInflictor, event ); } } void CAchievementMgr::OnAchievementEvent( int iAchievementID, int iCount ) { // have we loaded the achievements yet? if ( m_mapAchievement.Count() ) { // handle event for specific achievement CBaseAchievement *pAchievement = GetAchievementByID( iAchievementID ); Assert( pAchievement ); if ( pAchievement ) { if ( !pAchievement->IsAchieved() ) { pAchievement->IncrementCount( iCount ); } } } } //----------------------------------------------------------------------------- // Purpose: called when a map-fired achievement event occurs //----------------------------------------------------------------------------- void CAchievementMgr::OnMapEvent( const char *pchEventName ) { Assert( pchEventName && *pchEventName ); if ( !pchEventName || !*pchEventName ) return; // see if this event matches the prefix for an achievement component FOR_EACH_VEC( m_vecComponentListeners, iAchievement ) { CBaseAchievement *pAchievement = m_vecComponentListeners[iAchievement]; Assert( pAchievement->m_pszComponentPrefix ); if ( 0 == Q_strncmp( pchEventName, pAchievement->m_pszComponentPrefix, pAchievement->m_iComponentPrefixLen ) ) { // prefix matches, tell the achievement a component was found pAchievement->OnComponentEvent( pchEventName ); return; } } // look through all the map event listeners FOR_EACH_VEC( m_vecMapEventListeners, iAchievement ) { CBaseAchievement *pAchievement = m_vecMapEventListeners[iAchievement]; pAchievement->OnMapEvent( pchEventName ); } } //----------------------------------------------------------------------------- // Purpose: Returns an achievement as it's abstract object. This interface is used by gameui.dll for getting achievement info. // Input : index - // Output : IBaseAchievement* //----------------------------------------------------------------------------- IAchievement* CAchievementMgr::GetAchievementByIndex( int index ) { Assert( m_vecAchievement.IsValidIndex(index) ); return (IAchievement*)m_vecAchievement[index]; } //----------------------------------------------------------------------------- // Purpose: Returns total achievement count. This interface is used by gameui.dll for getting achievement info. // Input : - // Output : Count of achievements in manager's vector. //----------------------------------------------------------------------------- int CAchievementMgr::GetAchievementCount() { return m_vecAchievement.Count(); } #if !defined(NO_STEAM) //----------------------------------------------------------------------------- // Purpose: called when stat download is complete //----------------------------------------------------------------------------- void CAchievementMgr::Steam_OnUserStatsReceived( UserStatsReceived_t *pUserStatsReceived ) { Assert( steamapicontext->SteamUserStats() ); if ( !steamapicontext->SteamUserStats() ) return; if ( cc_achievement_debug.GetInt() > 0 ) { Msg( "CAchievementMgr::Steam_OnUserStatsReceived: result = %i\n", pUserStatsReceived->m_eResult ); } if ( pUserStatsReceived->m_eResult != k_EResultOK ) { DevMsg( "CTFSteamStats: failed to download stats from Steam, EResult %d\n", pUserStatsReceived->m_eResult ); return; } UpdateStateFromSteam_Internal(); } //----------------------------------------------------------------------------- // Purpose: called when stat upload is complete //----------------------------------------------------------------------------- void CAchievementMgr::Steam_OnUserStatsStored( UserStatsStored_t *pUserStatsStored ) { if ( cc_achievement_debug.GetInt() > 0 ) { Msg( "CAchievementMgr::Steam_OnUserStatsStored: result = %i\n", pUserStatsStored->m_eResult ); } if ( k_EResultOK != pUserStatsStored->m_eResult && k_EResultInvalidParam != pUserStatsStored->m_eResult ) { // We had some failure (like not connected or timeout) that means the stats were not stored. Redirty to try again SetDirty( true ); } else { if ( k_EResultInvalidParam == pUserStatsStored->m_eResult ) { // for whatever reason, stats and achievements were rejected by Steam. In order to remain in // synch, we get the current values/state from Steam. UpdateStateFromSteam_Internal(); // In the case of k_EResultInvalidParam, some of the stats may have been stored, so we should still fall through to // fire events related to achievements being awarded. } while ( m_AchievementsAwarded.Count() > 0 ) { #ifndef GAME_DLL // send a message to the server about our achievement if ( g_pGameRules && g_pGameRules->IsMultiplayer() ) { C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer(); if ( pLocalPlayer ) { int nAchievementID = m_AchievementsAwarded[0]; CBaseAchievement* pAchievement = GetAchievementByID( nAchievementID ); // verify that it is still achieved (it could have been rejected by Steam) if ( pAchievement->IsAchieved() ) { // Get the unlocked time from Steam uint32 unlockTime; bool bAchieved; bool bRet = steamapicontext->SteamUserStats()->GetAchievementAndUnlockTime( pAchievement->GetName(), &bAchieved, &unlockTime ); if ( bRet && bAchieved ) { // set the unlock time pAchievement->SetUnlockTime( unlockTime ); } KeyValues *kv = new KeyValues( "AchievementEarned" ); kv->SetInt( "achievementID", nAchievementID ); engine->ServerCmdKeyValues( kv ); } } } #endif m_AchievementsAwarded.Remove( 0 ); } CheckMetaAchievements(); } } #endif // !defined(NO_STEAM) void CAchievementMgr::CheckMetaAchievements( void ) { // Check the meta completion achievements. // Only one iteration over the achievements set is necessary. FOR_EACH_MAP( m_mapMetaAchievement, iAchievement ) { CAchievement_AchievedCount* pMetaAchievement = m_mapMetaAchievement[iAchievement]; if ( !pMetaAchievement || pMetaAchievement->IsAchieved() ) continue; int iAchieved = 0; for ( int i=pMetaAchievement->GetLowRange(); i<=pMetaAchievement->GetHighRange(); i++ ) { CBaseAchievement* pAchievement = GetAchievementByID( i ); if ( pAchievement && pAchievement->IsAchieved() ) { iAchieved++; } } if ( iAchieved >= pMetaAchievement->GetNumRequired() ) { pMetaAchievement->IncrementCount(); } } } void CAchievementMgr::ResetAchievement_Internal( CBaseAchievement *pAchievement ) { Assert( pAchievement ); #ifndef NO_STEAM if ( steamapicontext->SteamUserStats() ) { steamapicontext->SteamUserStats()->ClearAchievement( pAchievement->GetName() ); } #endif pAchievement->SetAchieved( false ); pAchievement->SetCount( 0 ); if ( pAchievement->HasComponents() ) { pAchievement->SetComponentBits( 0 ); } pAchievement->SetProgressShown( 0 ); pAchievement->StopListeningForAllEvents(); if ( pAchievement->IsActive() ) { pAchievement->ListenForEvents(); } } void CAchievementMgr::SetAchievementThink( CBaseAchievement *pAchievement, float flThinkTime ) { // Is the achievement already in the think list? int iCount = m_vecThinkListeners.Count(); for ( int i = 0; i < iCount; i++ ) { if ( m_vecThinkListeners[i].pAchievement == pAchievement ) { if ( flThinkTime == THINK_CLEAR ) { m_vecThinkListeners.Remove(i); return; } m_vecThinkListeners[i].m_flThinkTime = gpGlobals->curtime + flThinkTime; return; } } if ( flThinkTime == THINK_CLEAR ) return; // Otherwise, add it to the list int iIdx = m_vecThinkListeners.AddToTail(); m_vecThinkListeners[iIdx].pAchievement = pAchievement; m_vecThinkListeners[iIdx].m_flThinkTime = gpGlobals->curtime + flThinkTime; } void CAchievementMgr::UpdateStateFromSteam_Internal() { #ifndef NO_STEAM // run through the achievements and set their achieved state according to Steam data FOR_EACH_MAP( m_mapAchievement, i ) { CBaseAchievement *pAchievement = m_mapAchievement[i]; bool bAchieved = false; uint32 unlockTime; // Get the achievement status, and the time it was unlocked if unlocked. // If the return value is true, but the unlock time is zero, that means it was unlocked before Steam // began tracking achievement unlock times (December 2009). Time is seconds since January 1, 1970. bool bRet = steamapicontext->SteamUserStats()->GetAchievementAndUnlockTime( pAchievement->GetName(), &bAchieved, &unlockTime ); if ( bRet ) { // set local achievement state pAchievement->SetAchieved( bAchieved ); pAchievement->SetUnlockTime(unlockTime); } else { DevMsg( "ISteamUserStats::GetAchievement failed for %s\n", pAchievement->GetName() ); } if ( pAchievement->StoreProgressInSteam() ) { int iValue; char pszProgressName[1024]; Q_snprintf( pszProgressName, 1024, "%s_STAT", pAchievement->GetStat() ); bRet = steamapicontext->SteamUserStats()->GetStat( pszProgressName, &iValue ); if ( bRet ) { pAchievement->SetCount( iValue ); pAchievement->EvaluateNewAchievement(); } else { DevMsg( "ISteamUserStats::GetStat failed to get progress value from Steam for achievement %s\n", pszProgressName ); } } } // send an event to anyone else who needs Steam user stat data IGameEvent *event = gameeventmanager->CreateEvent( "user_data_downloaded" ); if ( event ) { #ifdef GAME_DLL gameeventmanager->FireEvent( event ); #else gameeventmanager->FireEventClientSide( event ); #endif } #endif } #ifdef CLIENT_DLL void MsgFunc_AchievementEvent( bf_read &msg ) { int iAchievementID = (int) msg.ReadShort(); int iCount = (int) msg.ReadShort(); CAchievementMgr *pAchievementMgr = dynamic_cast( engine->GetAchievementMgr() ); if ( !pAchievementMgr ) return; pAchievementMgr->OnAchievementEvent( iAchievementID, iCount ); } #if defined(_DEBUG) || defined(STAGING_ONLY) || DEBUG_ACHIEVEMENTS_IN_RELEASE CON_COMMAND_F( achievement_reset_all, "Clears all achievements", FCVAR_CHEAT ) { CAchievementMgr *pAchievementMgr = dynamic_cast( engine->GetAchievementMgr() ); if ( !pAchievementMgr ) return; pAchievementMgr->ResetAchievements(); } CON_COMMAND_F( achievement_reset, " Clears specified achievement", FCVAR_CHEAT ) { CAchievementMgr *pAchievementMgr = dynamic_cast( engine->GetAchievementMgr() ); if ( !pAchievementMgr ) return; if ( 2 != args.ArgC() ) { Msg( "Usage: achievement_reset \n" ); return; } CBaseAchievement *pAchievement = pAchievementMgr->GetAchievementByName( args[1] ); if ( !pAchievement ) { Msg( "Achievement %s not found\n", args[1] ); return; } pAchievementMgr->ResetAchievement( pAchievement->GetAchievementID() ); } CON_COMMAND_F( achievement_status, "Shows status of all achievement", FCVAR_CHEAT ) { CAchievementMgr *pAchievementMgr = dynamic_cast( engine->GetAchievementMgr() ); if ( !pAchievementMgr ) return; pAchievementMgr->PrintAchievementStatus(); } CON_COMMAND_F( achievement_unlock, " Unlocks achievement", FCVAR_CHEAT ) { CAchievementMgr *pAchievementMgr = dynamic_cast( engine->GetAchievementMgr() ); if ( !pAchievementMgr ) return; if ( 2 != args.ArgC() ) { Msg( "Usage: achievement_unlock \n" ); return; } CBaseAchievement *pAchievement = pAchievementMgr->GetAchievementByName( args[1] ); if ( !pAchievement ) { Msg( "Achievement %s not found\n", args[1] ); return; } pAchievementMgr->AwardAchievement( pAchievement->GetAchievementID() ); } CON_COMMAND_F( achievement_unlock_all, "Unlocks all achievements", FCVAR_CHEAT ) { CAchievementMgr *pAchievementMgr = dynamic_cast( engine->GetAchievementMgr() ); if ( !pAchievementMgr ) return; int iCount = pAchievementMgr->GetAchievementCount(); for ( int i = 0; i < iCount; i++ ) { IAchievement *pAchievement = pAchievementMgr->GetAchievementByIndex( i ); if ( !pAchievement->IsAchieved() ) { pAchievementMgr->AwardAchievement( pAchievement->GetAchievementID() ); } } } CON_COMMAND_F( achievement_evaluate, " Causes failable achievement to be evaluated", FCVAR_CHEAT ) { CAchievementMgr *pAchievementMgr = dynamic_cast( engine->GetAchievementMgr() ); if ( !pAchievementMgr ) return; if ( 2 != args.ArgC() ) { Msg( "Usage: achievement_evaluate \n" ); return; } CBaseAchievement *pAchievement = pAchievementMgr->GetAchievementByName( args[1] ); if ( !pAchievement ) { Msg( "Achievement %s not found\n", args[1] ); return; } CFailableAchievement *pFailableAchievement = dynamic_cast( pAchievement ); if ( !pFailableAchievement ) { Msg( "Achievement %s is not failable\n", args[1] ); return; } pFailableAchievement->OnEvaluationEvent(); } CON_COMMAND_F( achievement_test_friend_count, "Counts the # of teammates on local player's friends list", FCVAR_CHEAT ) { CAchievementMgr *pAchievementMgr = dynamic_cast( engine->GetAchievementMgr() ); if ( !pAchievementMgr ) return; if ( 2 != args.ArgC() ) { Msg( "Usage: achievement_test_friend_count \n" ); return; } int iMinFriends = atoi( args[1] ); bool bRet = CalcPlayersOnFriendsList( iMinFriends ); Msg( "You %s have at least %d friends in the game.\n", bRet ? "do" : "do not", iMinFriends ); } CON_COMMAND_F( achievement_test_clan_count, "Determines if specified # of teammates belong to same clan w/local player", FCVAR_CHEAT ) { CAchievementMgr *pAchievementMgr = dynamic_cast( engine->GetAchievementMgr() ); if ( !pAchievementMgr ) return; if ( 2 != args.ArgC() ) { Msg( "Usage: achievement_test_clan_count <# of clan teammates>\n" ); return; } int iClanPlayers = atoi( args[1] ); bool bRet = CalcHasNumClanPlayers( iClanPlayers ); Msg( "There %s %d players who you're in a Steam group with.\n", bRet ? "are" : "are not", iClanPlayers ); } CON_COMMAND_F( achievement_mark_dirty, "Mark achievement data as dirty", FCVAR_CHEAT ) { CAchievementMgr *pAchievementMgr = dynamic_cast( engine->GetAchievementMgr() ); if ( !pAchievementMgr ) return; pAchievementMgr->SetDirty( true ); } #endif // _DEBUG #endif // CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: helper function to get entity model name //----------------------------------------------------------------------------- const char *GetModelName( CBaseEntity *pBaseEntity ) { CBaseAnimating *pBaseAnimating = dynamic_cast( pBaseEntity ); if ( pBaseAnimating ) { CStudioHdr *pStudioHdr = pBaseAnimating->GetModelPtr(); if ( pStudioHdr ) { return pStudioHdr->pszName(); } } return ""; }