2023-06-18 12:24:47 +00:00
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-2.0 OR GPL-3.0 OR CC-BY-NC-ND-4.0)
// Derived from Duckstation's RetroAchievements implementation by stenzek as can be seen
// above, relicensed to GPL 2.0.
2024-04-03 14:21:31 +00:00
// Actually after the rc_client rewrite, barely anything of that remains.
// The PSP hash function:
// md5_init
// md5_hash(PSP_GAME/PARAM.SFO)
// md5_hash(PSP_GAME/EBOOT.BIN)
// hash = md5_finalize()
2023-06-15 20:17:27 +00:00
# include <algorithm>
# include <atomic>
# include <cstdarg>
# include <cstdlib>
# include <ctime>
# include <functional>
2023-07-10 08:39:44 +00:00
# include <set>
2023-06-15 20:17:27 +00:00
# include <string>
# include <vector>
2023-06-16 08:31:16 +00:00
# include <mutex>
2023-06-15 20:17:27 +00:00
2023-06-15 11:40:37 +00:00
# include "ext/rcheevos/include/rcheevos.h"
2023-06-27 21:31:15 +00:00
# include "ext/rcheevos/include/rc_client.h"
2024-04-05 09:07:57 +00:00
# include "ext/rcheevos/include/rc_client_raintegration.h"
2023-06-15 14:40:30 +00:00
# include "ext/rcheevos/include/rc_api_user.h"
2023-06-15 20:17:27 +00:00
# include "ext/rcheevos/include/rc_api_info.h"
# include "ext/rcheevos/include/rc_api_request.h"
# include "ext/rcheevos/include/rc_api_runtime.h"
# include "ext/rcheevos/include/rc_api_user.h"
# include "ext/rcheevos/include/rc_url.h"
2023-06-15 11:40:37 +00:00
2023-06-15 20:17:27 +00:00
# include "ext/rapidjson/include/rapidjson/document.h"
2024-04-03 14:21:31 +00:00
# include "Common/Crypto/md5.h"
2023-06-15 20:17:27 +00:00
# include "Common/Log.h"
# include "Common/File/Path.h"
2023-06-16 08:31:16 +00:00
# include "Common/File/FileUtil.h"
2023-06-15 20:17:27 +00:00
# include "Common/Net/HTTPClient.h"
2023-06-30 15:15:49 +00:00
# include "Common/System/OSD.h"
# include "Common/System/System.h"
2023-06-26 08:01:20 +00:00
# include "Common/System/NativeApp.h"
2023-06-15 21:27:38 +00:00
# include "Common/TimeUtil.h"
# include "Common/Data/Text/I18n.h"
# include "Common/Serialize/Serializer.h"
2023-06-27 21:31:15 +00:00
# include "Common/Serialize/SerializeFuncs.h"
2023-06-27 16:00:50 +00:00
# include "Common/StringUtils.h"
2023-06-16 22:00:57 +00:00
# include "Common/Crypto/md5.h"
2023-06-18 12:43:38 +00:00
# include "Common/UI/IconCache.h"
2023-06-15 21:27:38 +00:00
# include "Core/MemMap.h"
# include "Core/Config.h"
2023-06-16 08:31:16 +00:00
# include "Core/CoreParameter.h"
2024-04-05 09:07:57 +00:00
# include "Core/Core.h"
2023-06-16 08:31:16 +00:00
# include "Core/System.h"
2024-04-05 09:07:57 +00:00
# include "Core/FileLoaders/LocalFileLoader.h"
# include "Core/FileSystems/BlockDevices.h"
# include "Core/ELF/ParamSFO.h"
2023-06-16 22:00:57 +00:00
# include "Core/FileSystems/MetaFileSystem.h"
2024-04-03 14:21:31 +00:00
# include "Core/FileSystems/ISOFileSystem.h"
2023-07-02 10:00:13 +00:00
# include "Core/RetroAchievements.h"
2023-06-16 08:31:16 +00:00
2024-04-05 09:07:57 +00:00
# if RC_CLIENT_SUPPORTS_RAINTEGRATION
# include "Windows/MainWindow.h"
# endif
2024-04-03 14:21:31 +00:00
static bool HashISOFile ( ISOFileSystem * fs , const std : : string filename , md5_context * md5 ) {
int handle = fs - > OpenFile ( filename , FILEACCESS_READ ) ;
if ( handle < 0 ) {
return false ;
}
2024-04-05 09:07:57 +00:00
uint32_t sz = ( uint32_t ) fs - > SeekFile ( handle , 0 , FILEMOVE_END ) ;
2024-04-03 14:21:31 +00:00
fs - > SeekFile ( handle , 0 , FILEMOVE_BEGIN ) ;
if ( ! sz ) {
return false ;
}
2024-04-04 10:30:25 +00:00
auto buffer = std : : make_unique < uint8_t [ ] > ( sz ) ;
2024-04-03 14:21:31 +00:00
if ( fs - > ReadFile ( handle , buffer . get ( ) , sz ) ! = sz ) {
return false ;
}
fs - > CloseFile ( handle ) ;
ppsspp_md5_update ( md5 , buffer . get ( ) , sz ) ;
return true ;
}
2024-04-29 18:32:53 +00:00
static std : : string FormatRCheevosMD5 ( uint8_t digest [ 16 ] ) {
char hashStr [ 33 ] ;
/* NOTE: sizeof(hash) is 4 because it's still treated like a pointer, despite specifying a size */
snprintf ( hashStr , 33 , " %02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x " ,
digest [ 0 ] , digest [ 1 ] , digest [ 2 ] , digest [ 3 ] , digest [ 4 ] , digest [ 5 ] , digest [ 6 ] , digest [ 7 ] ,
digest [ 8 ] , digest [ 9 ] , digest [ 10 ] , digest [ 11 ] , digest [ 12 ] , digest [ 13 ] , digest [ 14 ] , digest [ 15 ]
) ;
return std : : string ( hashStr ) ;
}
2024-04-03 14:21:31 +00:00
// Consumes the blockDevice.
2024-04-04 10:30:25 +00:00
// If failed, returns an empty string, otherwise a 32-character string with the hash in hex format.
2024-04-29 18:32:53 +00:00
static std : : string ComputePSPISOHash ( BlockDevice * blockDevice ) {
2024-04-03 14:21:31 +00:00
md5_context md5 ;
ppsspp_md5_starts ( & md5 ) ;
SequentialHandleAllocator alloc ;
{
2024-04-04 10:30:25 +00:00
// ISOFileSystem takes owneship of the blockDevice here.
auto fs = std : : make_unique < ISOFileSystem > ( & alloc , blockDevice ) ;
2024-04-03 14:21:31 +00:00
if ( ! HashISOFile ( fs . get ( ) , " PSP_GAME/PARAM.SFO " , & md5 ) ) {
return std : : string ( ) ;
}
if ( ! HashISOFile ( fs . get ( ) , " PSP_GAME/SYSDIR/EBOOT.BIN " , & md5 ) ) {
return std : : string ( ) ;
}
}
uint8_t digest [ 16 ] ;
ppsspp_md5_finish ( & md5 , digest ) ;
2024-04-29 18:32:53 +00:00
return FormatRCheevosMD5 ( digest ) ;
}
2024-04-03 14:21:31 +00:00
2024-04-29 18:32:53 +00:00
static std : : string ComputePSPHomebrewHash ( FileLoader * fileLoader ) {
md5_context md5 ;
ppsspp_md5_starts ( & md5 ) ;
2024-04-29 21:31:46 +00:00
// Cap the data we read to 64MB (MAX_BUFFER_SIZE in rcheevos' hash.c), and hash that.
2024-04-29 18:32:53 +00:00
std : : vector < uint8_t > buffer ;
2024-04-29 21:31:46 +00:00
size_t fileSize = std : : min ( ( s64 ) ( 1024 * 1024 * 64 ) , fileLoader - > FileSize ( ) ) ;
2024-04-29 18:32:53 +00:00
buffer . resize ( fileSize ) ;
fileLoader - > ReadAt ( 0 , fileSize , buffer . data ( ) , FileLoader : : Flags : : NONE ) ;
ppsspp_md5_update ( & md5 , buffer . data ( ) , ( int ) buffer . size ( ) ) ;
uint8_t digest [ 16 ] ;
ppsspp_md5_finish ( & md5 , digest ) ;
return FormatRCheevosMD5 ( digest ) ;
2024-04-03 14:21:31 +00:00
}
2023-07-10 17:26:41 +00:00
static inline const char * DeNull ( const char * ptr ) {
return ptr ? ptr : " " ;
}
2023-06-16 14:29:44 +00:00
void OnAchievementsLoginStateChange ( ) {
2023-09-30 09:21:22 +00:00
System_PostUIMessage ( UIMessage : : ACHIEVEMENT_LOGIN_STATE_CHANGE ) ;
2023-06-16 14:29:44 +00:00
}
2023-06-15 21:27:38 +00:00
2023-06-15 20:17:27 +00:00
namespace Achievements {
2023-06-26 08:01:20 +00:00
// It's the name of the secret, not a secret name - the value is not secret :)
2023-12-28 13:00:01 +00:00
static const char * const RA_TOKEN_SECRET_NAME = " retroachievements " ;
2023-06-26 08:01:20 +00:00
2023-06-27 07:47:35 +00:00
static Achievements : : Statistics g_stats ;
2023-06-20 21:16:12 +00:00
2023-06-18 12:43:38 +00:00
const std : : string g_gameIconCachePrefix = " game: " ;
const std : : string g_iconCachePrefix = " badge: " ;
2024-04-05 09:07:57 +00:00
Path g_gamePath ;
2023-06-27 21:31:15 +00:00
std : : string s_game_hash ;
2023-06-27 07:47:35 +00:00
2023-07-10 08:39:44 +00:00
std : : set < uint32_t > g_activeChallenges ;
2023-07-03 07:18:25 +00:00
bool g_isIdentifying = false ;
2023-07-13 10:10:20 +00:00
bool g_isLoggingIn = false ;
2023-12-03 16:21:14 +00:00
bool g_hasRichPresence = false ;
2023-07-21 23:30:20 +00:00
int g_loginResult ;
2023-07-03 07:18:25 +00:00
2023-08-28 10:15:24 +00:00
double g_lastLoginAttemptTime ;
2023-06-27 21:31:15 +00:00
// rc_client implementation
static rc_client_t * g_rcClient ;
2023-08-25 14:36:29 +00:00
static const std : : string g_RAImageID = " I_RETROACHIEVEMENTS_LOGO " ;
2023-08-28 10:15:24 +00:00
constexpr double LOGIN_ATTEMPT_INTERVAL_S = 10.0 ;
2023-06-15 11:40:37 +00:00
2023-06-27 21:31:15 +00:00
# define PSP_MEMORY_OFFSET 0x08000000
2023-06-15 20:17:27 +00:00
2023-08-28 10:15:24 +00:00
static void TryLoginByToken ( bool isInitialAttempt ) ;
2023-06-27 21:31:15 +00:00
rc_client_t * GetClient ( ) {
return g_rcClient ;
2023-06-15 14:40:30 +00:00
}
2023-06-27 21:31:15 +00:00
bool IsLoggedIn ( ) {
2023-07-24 10:00:16 +00:00
return rc_client_get_user_info ( g_rcClient ) ! = nullptr & & ! g_isLoggingIn ;
2023-06-27 21:31:15 +00:00
}
2023-06-15 20:17:27 +00:00
2023-12-03 15:21:31 +00:00
// This is the RetroAchievements game ID, rather than the PSP game ID.
static u32 GetGameID ( ) {
if ( ! g_rcClient ) {
return 0 ;
}
const rc_client_game_t * info = rc_client_get_game_info ( g_rcClient ) ;
if ( ! info ) {
return 0 ;
}
return info - > id ; // 0 if not identified
}
2023-07-02 22:14:23 +00:00
bool EncoreModeActive ( ) {
if ( ! g_rcClient ) {
return false ;
}
return rc_client_get_encore_mode_enabled ( g_rcClient ) ;
}
2023-07-03 20:17:07 +00:00
bool UnofficialEnabled ( ) {
if ( ! g_rcClient ) {
return false ;
}
return rc_client_get_unofficial_enabled ( g_rcClient ) ;
}
2023-12-03 15:41:29 +00:00
bool HardcoreModeActive ( ) {
2023-07-02 22:47:54 +00:00
if ( ! g_rcClient ) {
return false ;
}
2023-12-03 15:43:49 +00:00
// See "Enabling Hardcore" under https://github.com/RetroAchievements/rcheevos/wiki/rc_client-integration.
return IsLoggedIn ( ) & & rc_client_get_hardcore_enabled ( g_rcClient ) & & rc_client_is_processing_required ( g_rcClient ) ;
2023-07-02 22:47:54 +00:00
}
2023-12-03 16:21:14 +00:00
size_t GetRichPresenceMessage ( char * buffer , size_t bufSize ) {
if ( ! IsLoggedIn ( ) | | ! rc_client_has_rich_presence ( g_rcClient ) ) {
return ( size_t ) - 1 ;
}
return rc_client_get_rich_presence_message ( g_rcClient , buffer , bufSize ) ;
}
2024-01-19 12:44:49 +00:00
bool WarnUserIfHardcoreModeActive ( bool isSaveStateAction , std : : string_view message ) {
2023-12-03 15:41:29 +00:00
if ( ! HardcoreModeActive ( ) | | ( isSaveStateAction & & g_Config . bAchievementsSaveStateInHardcoreMode ) ) {
2023-07-02 22:47:54 +00:00
return false ;
}
2024-01-19 12:44:49 +00:00
std : : string_view showMessage = message ;
if ( message . empty ( ) ) {
2023-07-02 22:47:54 +00:00
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
2023-11-30 16:33:14 +00:00
showMessage = ac - > T ( " This feature is not available in Hardcore Mode " ) ;
2023-07-02 22:47:54 +00:00
}
2023-08-25 14:36:29 +00:00
g_OSD . Show ( OSDType : : MESSAGE_WARNING , showMessage , " " , g_RAImageID , 3.0f ) ;
2023-07-02 22:47:54 +00:00
return true ;
2023-06-27 12:58:39 +00:00
}
2023-07-03 07:18:25 +00:00
bool IsBlockingExecution ( ) {
2023-07-24 10:00:16 +00:00
if ( g_isLoggingIn | | g_isIdentifying ) {
// Useful for debugging race conditions.
2024-07-14 12:42:59 +00:00
// INFO_LOG(Log::Achievements, "isLoggingIn: %d isIdentifying: %d", (int)g_isLoggingIn, (int)g_isIdentifying);
2023-07-24 10:00:16 +00:00
}
return g_isLoggingIn | | g_isIdentifying ;
2023-07-03 07:18:25 +00:00
}
2023-07-02 22:47:54 +00:00
bool IsActive ( ) {
return GetGameID ( ) ! = 0 ;
}
2024-10-10 12:10:30 +00:00
# ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
2024-04-05 19:00:12 +00:00
static void raintegration_write_memory_handler ( uint32_t address , uint8_t * buffer , uint32_t num_bytes , rc_client_t * client ) {
// convert_retroachievements_address_to_real_address
uint32_t realAddress = address + PSP_MEMORY_OFFSET ;
uint8_t * writePtr = Memory : : GetPointerWriteRange ( realAddress , num_bytes ) ;
if ( writePtr ) {
memcpy ( writePtr , buffer , num_bytes ) ;
}
}
2024-10-10 12:10:30 +00:00
# endif
2023-06-27 21:31:15 +00:00
static uint32_t read_memory_callback ( uint32_t address , uint8_t * buffer , uint32_t num_bytes , rc_client_t * client ) {
// Achievements are traditionally defined relative to the base of main memory of the emulated console.
// This is some kind of RetroArch-related legacy. In the PSP's case, this is simply a straight offset of 0x08000000.
2023-07-10 14:11:02 +00:00
uint32_t orig_address = address ;
2023-06-27 21:31:15 +00:00
address + = PSP_MEMORY_OFFSET ;
2023-06-15 20:17:27 +00:00
2023-07-16 07:08:31 +00:00
if ( ! Memory : : ValidSize ( address , num_bytes ) ) {
2023-06-27 21:31:15 +00:00
// Some achievement packs are really, really spammy.
// So we'll just count the bad accesses.
Achievements : : g_stats . badMemoryAccessCount + + ;
if ( g_Config . bAchievementsLogBadMemReads ) {
2024-07-14 12:42:59 +00:00
WARN_LOG ( Log : : G3D , " RetroAchievements PeekMemory: Bad address %08x (%d bytes) (%08x was passed in) " , address , num_bytes , orig_address ) ;
2023-06-15 20:17:27 +00:00
}
2023-07-10 14:11:02 +00:00
2023-07-16 15:05:08 +00:00
// This tells rcheevos that the access was bad, which should now be handled properly.
2023-08-09 14:16:39 +00:00
return 0 ;
2023-06-15 20:17:27 +00:00
}
2023-07-16 07:08:31 +00:00
Memory : : MemcpyUnchecked ( buffer , address , num_bytes ) ;
return num_bytes ;
2023-06-15 20:17:27 +00:00
}
2023-06-27 21:31:15 +00:00
// This is the HTTP request dispatcher that is provided to the rc_client. Whenever the client
// needs to talk to the server, it will call this function.
static void server_call_callback ( const rc_api_request_t * request ,
rc_client_server_callback_t callback , void * callback_data , rc_client_t * client )
{
// If post data is provided, we need to make a POST request, otherwise, a GET request will suffice.
2023-07-18 13:52:14 +00:00
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
2023-06-27 21:31:15 +00:00
if ( request - > post_data ) {
2023-07-21 20:04:05 +00:00
std : : shared_ptr < http : : Request > download = g_DownloadManager . AsyncPostWithCallback ( std : : string ( request - > url ) , std : : string ( request - > post_data ) , " application/x-www-form-urlencoded " , http : : ProgressBarMode : : DELAYED , [ = ] ( http : : Request & download ) {
2023-06-27 21:31:15 +00:00
std : : string buffer ;
download . buffer ( ) . TakeAll ( & buffer ) ;
rc_api_server_response_t response { } ;
response . body = buffer . c_str ( ) ;
response . body_length = buffer . size ( ) ;
response . http_status_code = download . ResultCode ( ) ;
callback ( & response , callback_data ) ;
2023-07-18 13:52:14 +00:00
} , ac - > T ( " Contacting RetroAchievements server... " ) ) ;
2023-06-27 21:31:15 +00:00
} else {
2023-07-21 20:04:05 +00:00
std : : shared_ptr < http : : Request > download = g_DownloadManager . StartDownloadWithCallback ( std : : string ( request - > url ) , Path ( ) , http : : ProgressBarMode : : DELAYED , [ = ] ( http : : Request & download ) {
2023-06-27 21:31:15 +00:00
std : : string buffer ;
download . buffer ( ) . TakeAll ( & buffer ) ;
rc_api_server_response_t response { } ;
response . body = buffer . c_str ( ) ;
response . body_length = buffer . size ( ) ;
response . http_status_code = download . ResultCode ( ) ;
callback ( & response , callback_data ) ;
2023-07-18 13:52:14 +00:00
} , ac - > T ( " Contacting RetroAchievements server... " ) ) ;
2023-06-27 21:31:15 +00:00
}
}
static void log_message_callback ( const char * message , const rc_client_t * client ) {
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " RetroAchievements: %s " , message ) ;
2023-06-27 21:31:15 +00:00
}
// For detailed documentation, see https://github.com/RetroAchievements/rcheevos/wiki/rc_client_set_event_handler.
static void event_handler_callback ( const rc_client_event_t * event , rc_client_t * client ) {
2023-07-03 12:39:49 +00:00
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
2023-06-27 21:31:15 +00:00
switch ( event - > type ) {
case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED :
// An achievement was earned by the player. The handler should notify the player that the achievement was earned.
2023-07-02 22:14:23 +00:00
g_OSD . ShowAchievementUnlocked ( event - > achievement - > id ) ;
2023-09-30 09:21:22 +00:00
System_PostUIMessage ( UIMessage : : REQUEST_PLAY_SOUND , " achievement_unlocked " ) ;
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Achievement unlocked: '%s' (%d) " , event - > achievement - > title , event - > achievement - > id ) ;
2023-06-27 21:31:15 +00:00
break ;
2023-07-12 17:18:56 +00:00
2023-06-27 21:31:15 +00:00
case RC_CLIENT_EVENT_GAME_COMPLETED :
2023-07-03 12:39:49 +00:00
{
2023-07-12 17:18:56 +00:00
// TODO: Do some zany fireworks!
2023-11-30 16:33:14 +00:00
// All achievements for the game have been earned. The handler should notify the player that the game was completed or mastered, depending on mode, hardcore or not.
2023-07-03 12:39:49 +00:00
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
const rc_client_game_t * gameInfo = rc_client_get_game_info ( g_rcClient ) ;
// TODO: Translation?
2023-07-16 14:16:47 +00:00
std : : string title = ApplySafeSubstitutions ( ac - > T ( " Mastered %1 " ) , gameInfo - > title ) ;
2023-11-27 00:10:52 +00:00
2023-07-03 12:39:49 +00:00
rc_client_user_game_summary_t summary ;
rc_client_get_user_game_summary ( g_rcClient , & summary ) ;
2023-10-08 21:22:28 +00:00
std : : string message = ApplySafeSubstitutions ( ac - > T ( " %1 achievements, %2 points " ) , summary . num_unlocked_achievements , summary . points_unlocked ) ;
2023-07-03 12:39:49 +00:00
2023-07-10 17:26:41 +00:00
g_OSD . Show ( OSDType : : MESSAGE_INFO , title , message , DeNull ( gameInfo - > badge_name ) , 10.0f ) ;
2023-07-03 12:39:49 +00:00
2023-09-30 09:21:22 +00:00
System_PostUIMessage ( UIMessage : : REQUEST_PLAY_SOUND , " achievement_unlocked " ) ;
2023-07-12 17:18:56 +00:00
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " %s " , message . c_str ( ) ) ;
2023-06-27 21:31:15 +00:00
break ;
2023-07-03 12:39:49 +00:00
}
2023-06-27 21:31:15 +00:00
case RC_CLIENT_EVENT_LEADERBOARD_STARTED :
case RC_CLIENT_EVENT_LEADERBOARD_FAILED :
2023-08-01 09:57:28 +00:00
{
bool started = event - > type = = RC_CLIENT_EVENT_LEADERBOARD_STARTED ;
// A leaderboard attempt has started. The handler may show a message with the leaderboard title and /or description indicating the attempt started.
const char * title = " " ;
const char * description = " " ;
// Hack around some problematic events in Burnout Legends. Hopefully this can be fixed in the backend.
if ( strlen ( event - > leaderboard - > title ) > 0 ) {
title = event - > leaderboard - > title ;
description = event - > leaderboard - > description ;
} else {
title = event - > leaderboard - > description ;
}
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Attempt %s: %s " , started ? " started " : " failed " , title ) ;
2023-08-01 09:57:28 +00:00
g_OSD . ShowLeaderboardStartEnd ( ApplySafeSubstitutions ( ac - > T ( started ? " %1: Attempt started " : " %1: Attempt failed " ) , title ) , description , started ) ;
2023-06-27 21:31:15 +00:00
break ;
2023-08-01 09:57:28 +00:00
}
2023-06-27 21:31:15 +00:00
case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED :
2023-08-01 09:57:28 +00:00
{
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Leaderboard result submitted: %s " , event - > leaderboard - > title ) ;
2024-07-18 21:15:27 +00:00
// Actually showing the result when we get the scoreboard message and have the new rank.
break ;
}
case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD :
{
const char * title = event - > leaderboard - > title ;
2023-08-01 09:57:28 +00:00
// Hack around some problematic events in Burnout Legends. Hopefully this can be fixed in the backend.
2024-07-18 21:15:27 +00:00
std : : string msg ;
const rc_client_leaderboard_scoreboard_t * scoreboard = event - > leaderboard_scoreboard ;
if ( event - > leaderboard_scoreboard ) {
msg = ApplySafeSubstitutions ( ac - > T ( " Submitted %1 for %2 " ) , DeNull ( scoreboard - > submitted_score ) , title ) ;
if ( ! strcmp ( scoreboard - > best_score , scoreboard - > submitted_score ) ) {
msg + = " : " ;
msg + = ApplySafeSubstitutions ( ac - > T ( " Rank: %1 " ) , scoreboard - > new_rank ) ;
} else {
msg + = " : " ;
msg + = ApplySafeSubstitutions ( ac - > T ( " Best: %1 " ) , scoreboard - > best_score ) ;
}
2023-08-01 09:57:28 +00:00
} else {
2024-07-18 21:15:27 +00:00
msg = ApplySafeSubstitutions ( ac - > T ( " Submitted %1 for %2 " ) , DeNull ( event - > leaderboard - > tracker_value ) , title ) ;
2023-08-01 09:57:28 +00:00
}
2024-07-18 21:15:27 +00:00
g_OSD . ShowLeaderboardSubmitted ( msg , " " ) ;
2023-09-30 09:21:22 +00:00
System_PostUIMessage ( UIMessage : : REQUEST_PLAY_SOUND , " leaderboard_submitted " ) ;
2023-06-27 21:31:15 +00:00
break ;
2023-08-01 09:57:28 +00:00
}
2023-06-27 21:31:15 +00:00
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW :
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Challenge indicator show: %s " , event - > achievement - > title ) ;
2023-07-03 12:39:49 +00:00
g_OSD . ShowChallengeIndicator ( event - > achievement - > id , true ) ;
2023-07-10 08:39:44 +00:00
g_activeChallenges . insert ( event - > achievement - > id ) ;
2023-06-27 21:31:15 +00:00
// A challenge achievement has become active. The handler should show a small version of the achievement icon
// to indicate the challenge is active.
break ;
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE :
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Challenge indicator hide: %s " , event - > achievement - > title ) ;
2023-07-03 12:39:49 +00:00
g_OSD . ShowChallengeIndicator ( event - > achievement - > id , false ) ;
2023-07-10 08:39:44 +00:00
g_activeChallenges . erase ( event - > achievement - > id ) ;
2023-06-27 21:31:15 +00:00
// The handler should hide the small version of the achievement icon that was shown by the corresponding RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW event.
break ;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW :
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Progress indicator show: %s, progress: '%s' (%f) " , event - > achievement - > title , event - > achievement - > measured_progress , event - > achievement - > measured_percent ) ;
2023-06-27 21:31:15 +00:00
// An achievement that tracks progress has changed the amount of progress that has been made.
// The handler should show a small version of the achievement icon along with the achievement->measured_progress text (for two seconds).
// Only one progress indicator should be shown at a time.
// If a progress indicator is already visible, it should be updated with the new icon and text, and the two second timer should be restarted.
2023-07-30 08:55:55 +00:00
g_OSD . ShowAchievementProgress ( event - > achievement - > id , true ) ;
break ;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE :
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Progress indicator update: %s, progress: '%s' (%f) " , event - > achievement - > title , event - > achievement - > measured_progress , event - > achievement - > measured_percent ) ;
2023-07-30 08:55:55 +00:00
g_OSD . ShowAchievementProgress ( event - > achievement - > id , true ) ;
break ;
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE :
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Progress indicator hide " ) ;
2023-07-30 08:55:55 +00:00
// An achievement that tracks progress has changed the amount of progress that has been made.
// The handler should show a small version of the achievement icon along with the achievement->measured_progress text (for two seconds).
// Only one progress indicator should be shown at a time.
// If a progress indicator is already visible, it should be updated with the new icon and text, and the two second timer should be restarted.
g_OSD . ShowAchievementProgress ( 0 , false ) ;
2023-06-27 21:31:15 +00:00
break ;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW :
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Leaderboard tracker show: '%s' (id %d) " , event - > leaderboard_tracker - > display , event - > leaderboard_tracker - > id ) ;
2023-06-27 21:31:15 +00:00
// A leaderboard_tracker has become active. The handler should show the tracker text on screen.
// Multiple active leaderboards may share a single tracker if they have the same definition and value.
// As such, the leaderboard tracker IDs are unique amongst the leaderboard trackers, and have no correlation to the active leaderboard(s).
// Use event->leaderboard_tracker->id for uniqueness checks, and display event->leaderboard_tracker->display (string)
2023-07-03 12:39:49 +00:00
g_OSD . ShowLeaderboardTracker ( event - > leaderboard_tracker - > id , event - > leaderboard_tracker - > display , true ) ;
2023-06-27 21:31:15 +00:00
break ;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE :
2023-07-03 12:39:49 +00:00
// A leaderboard_tracker has become inactive. The handler should hide the tracker text from the screen.
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Leaderboard tracker hide: '%s' (id %d) " , event - > leaderboard_tracker - > display , event - > leaderboard_tracker - > id ) ;
2023-07-03 12:39:49 +00:00
g_OSD . ShowLeaderboardTracker ( event - > leaderboard_tracker - > id , nullptr , false ) ;
2023-06-27 21:31:15 +00:00
break ;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE :
// A leaderboard_tracker value has been updated. The handler should update the tracker text on the screen.
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Leaderboard tracker update: '%s' (id %d) " , event - > leaderboard_tracker - > display , event - > leaderboard_tracker - > id ) ;
2023-07-02 22:47:54 +00:00
g_OSD . ShowLeaderboardTracker ( event - > leaderboard_tracker - > id , event - > leaderboard_tracker - > display , true ) ;
2023-06-27 21:31:15 +00:00
break ;
case RC_CLIENT_EVENT_RESET :
2024-07-14 12:42:59 +00:00
WARN_LOG ( Log : : Achievements , " Resetting game due to achievement setting change! " ) ;
2023-11-30 16:33:14 +00:00
// Hardcore mode was enabled, or something else that forces a game reset.
2023-09-30 09:21:22 +00:00
System_PostUIMessage ( UIMessage : : REQUEST_GAME_RESET ) ;
2023-06-27 21:31:15 +00:00
break ;
case RC_CLIENT_EVENT_SERVER_ERROR :
2024-07-14 12:42:59 +00:00
ERROR_LOG ( Log : : Achievements , " Server error: %s: %s " , event - > server_error - > api , event - > server_error - > error_message ) ;
2023-08-25 14:36:29 +00:00
g_OSD . Show ( OSDType : : MESSAGE_ERROR , " Server error " , " " , g_RAImageID ) ;
2023-06-27 21:31:15 +00:00
break ;
default :
2024-07-14 12:42:59 +00:00
WARN_LOG ( Log : : Achievements , " Unhandled rc_client event %d, ignoring " , event - > type ) ;
2023-06-27 21:31:15 +00:00
break ;
}
2023-06-15 20:17:27 +00:00
}
2023-07-13 10:10:20 +00:00
static void login_token_callback ( int result , const char * error_message , rc_client_t * client , void * userdata ) {
2023-08-28 10:15:24 +00:00
bool isInitialAttempt = userdata ! = nullptr ;
2023-07-13 10:10:20 +00:00
switch ( result ) {
case RC_OK :
2023-08-28 10:15:24 +00:00
{
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Successful login by token. " ) ;
2023-07-13 10:10:20 +00:00
OnAchievementsLoginStateChange ( ) ;
2023-08-28 10:15:24 +00:00
if ( ! isInitialAttempt ) {
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
g_OSD . Show ( OSDType : : MESSAGE_SUCCESS , ac - > T ( " Reconnected to RetroAchievements. " ) , " " , g_RAImageID ) ;
}
2023-07-13 10:10:20 +00:00
break ;
2023-08-28 10:15:24 +00:00
}
2023-07-13 10:10:20 +00:00
case RC_NO_RESPONSE :
{
2023-08-28 10:15:24 +00:00
if ( isInitialAttempt ) {
auto di = GetI18NCategory ( I18NCat : : DIALOG ) ;
g_OSD . Show ( OSDType : : MESSAGE_WARNING , di - > T ( " Failed to connect to server, check your internet connection. " ) , " " , g_RAImageID ) ;
}
2023-07-13 10:10:20 +00:00
break ;
}
2023-10-03 12:57:27 +00:00
case RC_ACCESS_DENIED :
case RC_INVALID_CREDENTIALS :
case RC_EXPIRED_TOKEN :
2023-07-13 10:10:20 +00:00
case RC_API_FAILURE :
2023-07-21 23:30:20 +00:00
case RC_INVALID_STATE :
2023-07-13 10:10:20 +00:00
case RC_MISSING_VALUE :
case RC_INVALID_JSON :
default :
2023-07-21 23:30:20 +00:00
{
2024-07-14 12:42:59 +00:00
ERROR_LOG ( Log : : Achievements , " Callback: Failure logging in via token: %d, %s " , result , error_message ) ;
2023-08-28 10:15:24 +00:00
if ( isInitialAttempt ) {
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
g_OSD . Show ( OSDType : : MESSAGE_WARNING , ac - > T ( " Failed logging in to RetroAchievements " ) , " " , g_RAImageID ) ;
}
2024-09-25 23:09:56 +00:00
// Clear the token.
if ( result = = RC_INVALID_CREDENTIALS | | result = = RC_EXPIRED_TOKEN ) {
g_Config . sAchievementsUserName . clear ( ) ;
NativeClearSecret ( RA_TOKEN_SECRET_NAME ) ;
g_loginResult = RC_OK ;
} else {
g_loginResult = result ;
}
2023-07-13 10:10:20 +00:00
OnAchievementsLoginStateChange ( ) ;
2024-09-25 23:09:56 +00:00
g_isLoggingIn = false ;
return ;
2023-07-13 10:10:20 +00:00
}
2023-07-21 23:30:20 +00:00
}
g_loginResult = result ;
2023-07-13 10:10:20 +00:00
g_isLoggingIn = false ;
}
2024-04-05 09:07:57 +00:00
bool RAIntegrationDirty ( ) {
# ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
return rc_client_raintegration_has_modifications ( g_rcClient ) ;
# else
return false ;
# endif
}
# ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
static void raintegration_get_game_name_handler ( char * buffer , uint32_t buffer_size , rc_client_t * client ) {
snprintf ( buffer , buffer_size , " %s " , g_gamePath . GetFilename ( ) . c_str ( ) ) ;
}
2024-04-05 19:00:12 +00:00
static void raintegration_write_memory_handler ( uint32_t address , uint8_t * buffer , uint32_t num_bytes , rc_client_t * client ) ;
2024-04-05 09:07:57 +00:00
static void raintegration_event_handler ( const rc_client_raintegration_event_t * event , rc_client_t * client ) {
switch ( event - > type ) {
case RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED :
// The checked state of one of the menu items has changed and should be reflected in the UI.
// Call the handy helper function if the menu was created by rc_client_raintegration_rebuild_submenu.
2024-04-06 10:18:28 +00:00
System_RunCallbackInWndProc ( [ ] ( void * vhWnd , void * userdata ) {
auto menuItem = reinterpret_cast < const rc_client_raintegration_menu_item_t * > ( userdata ) ;
rc_client_raintegration_update_menu_item ( g_rcClient , menuItem ) ;
} , reinterpret_cast < void * > ( reinterpret_cast < int64_t > ( event - > menu_item ) ) ) ;
2024-04-05 09:07:57 +00:00
break ;
case RC_CLIENT_RAINTEGRATION_EVENT_PAUSE :
// The toolkit has hit a breakpoint and wants to pause the emulator. Do so.
2024-11-01 21:52:47 +00:00
Core_Break ( " ra_breakpoint " ) ;
2024-04-05 09:07:57 +00:00
break ;
case RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED :
// Hardcore mode has been changed (either directly by the user, or disabled through the use of the tools).
// The frontend doesn't necessarily need to know that this value changed, they can still query it whenever
// it's appropriate, but the event lets the frontend do things like enable/disable rewind or cheats.
2024-04-05 20:41:08 +00:00
g_Config . bAchievementsHardcoreMode = rc_client_get_hardcore_enabled ( client ) ;
2024-04-05 09:07:57 +00:00
break ;
default :
2024-07-14 12:42:59 +00:00
ERROR_LOG ( Log : : Achievements , " Unsupported raintegration event %u \n " , event - > type ) ;
2024-04-05 09:07:57 +00:00
break ;
}
}
static void load_integration_callback ( int result , const char * error_message , rc_client_t * client , void * userdata ) {
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
// If DLL not present, do nothing. User can still play without the toolkit.
switch ( result ) {
case RC_OK :
{
2024-04-05 16:21:02 +00:00
// DLL was loaded correctly.
2024-04-05 09:07:57 +00:00
g_OSD . Show ( OSDType : : MESSAGE_SUCCESS , ac - > T ( " RAIntegration DLL loaded. " ) ) ;
2024-04-29 22:49:55 +00:00
rc_client_raintegration_set_console_id ( g_rcClient , RC_CONSOLE_PSP ) ;
2024-04-05 09:07:57 +00:00
rc_client_raintegration_set_event_handler ( g_rcClient , & raintegration_event_handler ) ;
rc_client_raintegration_set_write_memory_function ( g_rcClient , & raintegration_write_memory_handler ) ;
rc_client_raintegration_set_get_game_name_function ( g_rcClient , & raintegration_get_game_name_handler ) ;
2024-04-06 10:04:45 +00:00
2024-04-06 10:14:53 +00:00
System_RunCallbackInWndProc ( [ ] ( void * vhWnd , void * userdata ) {
HWND hWnd = reinterpret_cast < HWND > ( vhWnd ) ;
rc_client_raintegration_rebuild_submenu ( g_rcClient , GetMenu ( hWnd ) ) ;
} , nullptr ) ;
2024-04-05 09:07:57 +00:00
break ;
}
case RC_MISSING_VALUE :
// This is fine, proceeding to login.
g_OSD . Show ( OSDType : : MESSAGE_WARNING , ac - > T ( " RAIntegration is enabled, but RAIntegration-x64.dll was not found. " ) ) ;
break ;
case RC_ABORTED :
// This is fine, proceeding to login.
g_OSD . Show ( OSDType : : MESSAGE_WARNING , ac - > T ( " Wrong version of RAIntegration-x64.dll? " ) ) ;
break ;
default :
g_OSD . Show ( OSDType : : MESSAGE_ERROR , StringFromFormat ( " RAIntegration init failed: %s " , error_message ) ) ;
// Bailing.
return ;
}
// Things are ready to load a game. If the DLL was initialized, calling rc_client_begin_load_game will be redirected
// through the DLL so the toolkit has access to the game data. Similarly, things like rc_create_leaderboard_list will
// be redirected through the DLL to reflect any local changes made by the user.
TryLoginByToken ( true ) ;
}
# endif
2023-06-27 21:31:15 +00:00
void Initialize ( ) {
2023-07-01 10:31:46 +00:00
if ( ! g_Config . bAchievementsEnable ) {
_dbg_assert_ ( ! g_rcClient ) ;
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Achievements are disabled, not initializing. " ) ;
2023-07-01 10:31:46 +00:00
return ;
}
2023-07-11 08:16:58 +00:00
_assert_msg_ ( ! g_rcClient , " Achievements already initialized " ) ;
2023-06-15 20:17:27 +00:00
2023-06-27 21:31:15 +00:00
g_rcClient = rc_client_create ( read_memory_callback , server_call_callback ) ;
2023-07-11 08:16:58 +00:00
if ( ! g_rcClient ) {
// Shouldn't happen really.
return ;
}
2023-06-27 21:31:15 +00:00
// Provide a logging function to simplify debugging
rc_client_enable_logging ( g_rcClient , RC_CLIENT_LOG_LEVEL_VERBOSE , log_message_callback ) ;
2023-07-21 08:49:01 +00:00
if ( ! System_GetPropertyBool ( SYSPROP_SUPPORTS_HTTPS ) ) {
// Disable SSL if not supported by our platform implementation.
rc_client_set_host ( g_rcClient , " http://retroachievements.org " ) ;
}
2023-06-15 20:17:27 +00:00
2023-06-27 21:31:15 +00:00
rc_client_set_event_handler ( g_rcClient , event_handler_callback ) ;
2023-06-15 20:17:27 +00:00
2024-04-05 20:41:08 +00:00
// Set initial settings properly. Hardcore mode is checked by RAIntegration.
rc_client_set_hardcore_enabled ( g_rcClient , g_Config . bAchievementsHardcoreMode ? 1 : 0 ) ;
rc_client_set_encore_mode_enabled ( g_rcClient , g_Config . bAchievementsEncoreMode ? 1 : 0 ) ;
rc_client_set_unofficial_enabled ( g_rcClient , g_Config . bAchievementsUnofficial ? 1 : 0 ) ;
2024-04-05 09:07:57 +00:00
# ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
if ( g_Config . bAchievementsEnableRAIntegration ) {
wchar_t szFilePath [ MAX_PATH ] ;
GetModuleFileNameW ( NULL , szFilePath , MAX_PATH ) ;
for ( int64_t i = wcslen ( szFilePath ) - 1 ; i > 0 ; i - - ) {
if ( szFilePath [ i ] = = ' \\ ' ) {
szFilePath [ i ] = ' \0 ' ;
break ;
}
}
HWND hWnd = ( HWND ) System_GetPropertyInt ( SYSPROP_MAIN_WINDOW_HANDLE ) ;
2024-04-05 16:16:46 +00:00
rc_client_begin_load_raintegration ( g_rcClient , szFilePath , hWnd , " PPSSPP " , PPSSPP_GIT_VERSION , & load_integration_callback , hWnd ) ;
2024-04-05 09:07:57 +00:00
return ;
}
# endif
2023-08-28 10:15:24 +00:00
TryLoginByToken ( true ) ;
2023-07-21 23:30:20 +00:00
}
bool HasToken ( ) {
return ! NativeLoadSecret ( RA_TOKEN_SECRET_NAME ) . empty ( ) ;
}
bool LoginProblems ( std : : string * errorString ) {
// TODO: Set error string.
return g_loginResult ! = RC_OK ;
}
2023-08-28 10:15:24 +00:00
static void TryLoginByToken ( bool isInitialAttempt ) {
2023-09-06 09:19:13 +00:00
if ( g_Config . sAchievementsUserName . empty ( ) ) {
// Don't even look for a token - without a username we can't login.
return ;
}
2023-06-27 21:31:15 +00:00
std : : string api_token = NativeLoadSecret ( RA_TOKEN_SECRET_NAME ) ;
if ( ! api_token . empty ( ) ) {
2023-07-13 10:10:20 +00:00
g_isLoggingIn = true ;
2023-08-28 10:15:24 +00:00
rc_client_begin_login_with_token ( g_rcClient , g_Config . sAchievementsUserName . c_str ( ) , api_token . c_str ( ) , & login_token_callback , ( void * ) isInitialAttempt ) ;
2023-06-15 20:17:27 +00:00
}
2023-06-27 21:31:15 +00:00
}
2023-06-15 20:17:27 +00:00
2023-06-27 21:31:15 +00:00
static void login_password_callback ( int result , const char * error_message , rc_client_t * client , void * userdata ) {
2023-07-12 17:21:08 +00:00
auto di = GetI18NCategory ( I18NCat : : DIALOG ) ;
2023-06-27 21:31:15 +00:00
switch ( result ) {
case RC_OK :
2023-06-15 20:17:27 +00:00
{
2023-06-27 21:31:15 +00:00
// Get the token and store it.
const rc_client_user_t * user = rc_client_get_user_info ( client ) ;
g_Config . sAchievementsUserName = user - > username ;
NativeSaveSecret ( RA_TOKEN_SECRET_NAME , std : : string ( user - > token ) ) ;
2023-07-02 22:14:23 +00:00
OnAchievementsLoginStateChange ( ) ;
2023-08-25 14:36:29 +00:00
g_OSD . Show ( OSDType : : MESSAGE_SUCCESS , di - > T ( " Logged in! " ) , " " , g_RAImageID ) ;
2023-06-27 21:31:15 +00:00
break ;
2023-06-15 20:17:27 +00:00
}
2023-07-13 09:24:37 +00:00
case RC_NO_RESPONSE :
{
auto di = GetI18NCategory ( I18NCat : : DIALOG ) ;
2023-08-25 14:36:29 +00:00
g_OSD . Show ( OSDType : : MESSAGE_WARNING , di - > T ( " Failed to connect to server, check your internet connection. " ) , " " , g_RAImageID ) ;
2023-07-13 09:24:37 +00:00
break ;
}
2023-06-27 21:31:15 +00:00
case RC_INVALID_STATE :
case RC_API_FAILURE :
case RC_MISSING_VALUE :
case RC_INVALID_JSON :
2023-10-03 12:57:27 +00:00
case RC_ACCESS_DENIED :
case RC_INVALID_CREDENTIALS :
case RC_EXPIRED_TOKEN :
2023-07-13 09:24:37 +00:00
default :
2023-07-12 17:21:08 +00:00
{
2024-07-14 12:42:59 +00:00
ERROR_LOG ( Log : : Achievements , " Failure logging in via password: %d, %s " , result , error_message ) ;
2023-08-25 14:36:29 +00:00
g_OSD . Show ( OSDType : : MESSAGE_WARNING , di - > T ( " Failed to log in, check your username and password. " ) , " " , g_RAImageID ) ;
2023-07-02 22:14:23 +00:00
OnAchievementsLoginStateChange ( ) ;
2023-06-27 21:31:15 +00:00
break ;
2023-06-15 20:17:27 +00:00
}
2023-07-12 17:21:08 +00:00
}
2023-06-15 21:27:38 +00:00
2023-07-18 13:13:44 +00:00
g_OSD . RemoveProgressBar ( " cheevos_async_login " , true , 0.1f ) ;
2023-07-21 23:30:20 +00:00
g_loginResult = RC_OK ; // For these, we don't want the "permanence" of the login-by-token failure, this prevents LoginProblems from returning true.
2023-07-13 10:10:20 +00:00
g_isLoggingIn = false ;
2023-06-15 20:17:27 +00:00
}
2023-06-27 21:31:15 +00:00
bool LoginAsync ( const char * username , const char * password ) {
2023-07-13 09:24:37 +00:00
auto di = GetI18NCategory ( I18NCat : : DIALOG ) ;
2024-04-05 09:07:57 +00:00
if ( IsLoggedIn ( ) | | std : : strlen ( username ) = = 0 | | std : : strlen ( password ) = = 0 )
2023-06-15 20:17:27 +00:00
return false ;
2023-07-18 13:13:44 +00:00
g_OSD . SetProgressBar ( " cheevos_async_login " , di - > T ( " Logging in... " ) , 0 , 0 , 0 , 0.0f ) ;
2023-07-13 09:24:37 +00:00
2023-07-13 10:10:20 +00:00
g_isLoggingIn = true ;
2023-06-27 21:31:15 +00:00
rc_client_begin_login_with_password ( g_rcClient , username , password , & login_password_callback , nullptr ) ;
2023-06-15 20:17:27 +00:00
return true ;
}
2023-06-27 21:31:15 +00:00
void Logout ( ) {
rc_client_logout ( g_rcClient ) ;
// remove from config
g_Config . sAchievementsUserName . clear ( ) ;
2024-09-25 23:09:56 +00:00
NativeClearSecret ( RA_TOKEN_SECRET_NAME ) ;
2023-06-27 21:31:15 +00:00
g_Config . Save ( " Achievements logout " ) ;
2023-07-10 08:39:44 +00:00
g_activeChallenges . clear ( ) ;
2023-07-21 23:30:20 +00:00
g_loginResult = RC_OK ; // Allow trying again
2023-07-11 08:05:55 +00:00
OnAchievementsLoginStateChange ( ) ;
2023-06-15 20:17:27 +00:00
}
2023-06-27 21:31:15 +00:00
void UpdateSettings ( ) {
2023-07-11 08:16:58 +00:00
if ( g_rcClient & & ! g_Config . bAchievementsEnable ) {
2023-06-27 21:31:15 +00:00
// we're done here
Shutdown ( ) ;
2023-06-15 20:17:27 +00:00
return ;
}
2023-07-11 08:16:58 +00:00
if ( ! g_rcClient & & g_Config . bAchievementsEnable ) {
2023-07-02 15:12:46 +00:00
// we just got enabled.
2023-06-27 21:31:15 +00:00
Initialize ( ) ;
2023-06-15 20:17:27 +00:00
}
}
2023-06-27 21:31:15 +00:00
bool Shutdown ( ) {
2023-07-10 08:39:44 +00:00
g_activeChallenges . clear ( ) ;
2024-04-05 09:07:57 +00:00
# ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
rc_client_unload_raintegration ( g_rcClient ) ;
# endif
2023-06-27 21:31:15 +00:00
rc_client_destroy ( g_rcClient ) ;
g_rcClient = nullptr ;
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Achievements shut down. " ) ;
2023-06-15 20:17:27 +00:00
return true ;
}
2023-06-27 21:31:15 +00:00
void ResetRuntime ( ) {
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Resetting rcheevos state... " ) ;
2023-06-27 21:31:15 +00:00
rc_client_reset ( g_rcClient ) ;
2023-07-10 08:39:44 +00:00
g_activeChallenges . clear ( ) ;
2023-06-15 20:17:27 +00:00
}
2023-06-27 21:31:15 +00:00
void FrameUpdate ( ) {
if ( ! g_rcClient )
2023-06-15 20:17:27 +00:00
return ;
2023-06-27 21:31:15 +00:00
rc_client_do_frame ( g_rcClient ) ;
2023-06-15 20:17:27 +00:00
}
2023-06-27 21:31:15 +00:00
void Idle ( ) {
rc_client_idle ( g_rcClient ) ;
2023-08-28 10:15:24 +00:00
double now = time_now_d ( ) ;
// If failed to log in, occasionally try again while the user is at the menu.
// Do not try if if in-game, that could get confusing.
2023-08-28 12:38:32 +00:00
if ( g_Config . bAchievementsEnable & & GetUIState ( ) = = UISTATE_MENU & & now > g_lastLoginAttemptTime + LOGIN_ATTEMPT_INTERVAL_S ) {
2023-08-28 10:15:24 +00:00
g_lastLoginAttemptTime = now ;
if ( g_rcClient & & IsLoggedIn ( ) ) {
return ; // All good.
}
2023-09-06 08:34:32 +00:00
if ( g_Config . sAchievementsUserName . empty ( ) | | g_isLoggingIn | | ! HasToken ( ) ) {
// Didn't try to login yet or is in the process of logging in. Also OK.
2023-08-28 10:15:24 +00:00
return ;
}
// In this situation, there's a token, but we're not logged in. Probably disrupted internet connection
// during startup.
// Let's make an attempt.
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Retrying login.. " ) ;
2023-08-28 10:15:24 +00:00
TryLoginByToken ( false ) ;
}
2023-06-15 20:17:27 +00:00
}
2023-06-27 21:31:15 +00:00
void DoState ( PointerWrap & p ) {
2023-07-01 10:31:46 +00:00
auto sw = p . Section ( " Achievements " , 0 , 1 ) ;
2023-06-15 21:27:38 +00:00
if ( ! sw ) {
// Save state is missing the section.
// Reset the runtime.
2023-07-01 10:31:46 +00:00
if ( HasAchievementsOrLeaderboards ( ) ) {
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
2023-08-25 14:36:29 +00:00
g_OSD . Show ( OSDType : : MESSAGE_WARNING , ac - > T ( " Save state loaded without achievement data " ) , " " , g_RAImageID , 5.0f , " " ) ;
2023-07-01 10:31:46 +00:00
}
rc_client_reset ( g_rcClient ) ;
2023-06-15 21:27:38 +00:00
return ;
}
2023-06-27 21:31:15 +00:00
uint32_t data_size = 0 ;
2023-07-01 10:31:46 +00:00
2023-07-02 22:14:23 +00:00
if ( ! IsActive ( ) ) {
2023-07-01 10:31:46 +00:00
Do ( p , data_size ) ;
2023-07-02 22:14:23 +00:00
if ( p . mode = = PointerWrap : : MODE_READ ) {
2024-07-14 12:42:59 +00:00
WARN_LOG ( Log : : Achievements , " Save state contained achievement data, but achievements are not active. Ignore. " ) ;
2023-07-02 22:14:23 +00:00
}
2023-07-01 10:31:46 +00:00
p . SkipBytes ( data_size ) ;
return ;
}
2023-06-27 21:31:15 +00:00
if ( p . mode = = PointerWrap : : MODE_MEASURE | | p . mode = = PointerWrap : : MODE_WRITE | | p . mode = = PointerWrap : : MODE_VERIFY | | p . mode = = PointerWrap : : MODE_NOOP ) {
data_size = ( uint32_t ) ( g_rcClient ? rc_client_progress_size ( g_rcClient ) : 0 ) ;
2023-06-15 20:17:27 +00:00
}
2023-06-27 21:31:15 +00:00
Do ( p , data_size ) ;
2023-06-15 20:17:27 +00:00
2023-06-27 21:31:15 +00:00
if ( data_size > 0 ) {
uint8_t * buffer = new uint8_t [ data_size ] ;
switch ( p . mode ) {
case PointerWrap : : MODE_NOOP :
case PointerWrap : : MODE_MEASURE :
case PointerWrap : : MODE_WRITE :
case PointerWrap : : MODE_VERIFY :
2023-07-01 10:31:46 +00:00
{
int retval = rc_client_serialize_progress ( g_rcClient , buffer ) ;
if ( retval ! = RC_OK ) {
2024-07-14 12:42:59 +00:00
ERROR_LOG ( Log : : Achievements , " Error %d serializing achievement data. Ignoring. " , retval ) ;
2023-07-01 10:31:46 +00:00
}
2023-06-27 21:31:15 +00:00
break ;
2023-06-15 20:17:27 +00:00
}
2023-07-11 23:11:09 +00:00
default :
break ;
2023-07-01 10:31:46 +00:00
}
2023-06-15 20:17:27 +00:00
2023-06-27 21:31:15 +00:00
DoArray ( p , buffer , data_size ) ;
2023-06-15 20:17:27 +00:00
2023-06-27 21:31:15 +00:00
switch ( p . mode ) {
case PointerWrap : : MODE_READ :
2023-07-01 10:31:46 +00:00
{
int retval = rc_client_deserialize_progress ( g_rcClient , buffer ) ;
if ( retval ! = RC_OK ) {
// TODO: What should we really do here?
2024-07-14 12:42:59 +00:00
ERROR_LOG ( Log : : Achievements , " Error %d deserializing achievement data. Ignoring. " , retval ) ;
2023-07-01 10:31:46 +00:00
}
2023-06-27 21:31:15 +00:00
break ;
2023-06-15 20:17:27 +00:00
}
2023-07-11 23:11:09 +00:00
default :
break ;
2023-07-01 10:31:46 +00:00
}
2023-06-27 21:31:15 +00:00
delete [ ] buffer ;
2023-07-01 10:31:46 +00:00
} else {
2023-07-02 22:14:23 +00:00
if ( IsActive ( ) ) {
2023-07-01 10:31:46 +00:00
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
2023-08-25 14:36:29 +00:00
g_OSD . Show ( OSDType : : MESSAGE_WARNING , ac - > T ( " Save state loaded without achievement data " ) , " " , g_RAImageID , 5.0f ) ;
2023-07-01 10:31:46 +00:00
}
rc_client_reset ( g_rcClient ) ;
2023-06-15 20:17:27 +00:00
}
}
2023-06-27 21:31:15 +00:00
bool HasAchievementsOrLeaderboards ( ) {
if ( ! g_rcClient ) {
return false ;
}
2023-07-02 22:14:23 +00:00
return IsActive ( ) ;
2023-06-15 20:17:27 +00:00
}
2023-06-27 21:31:15 +00:00
void DownloadImageIfMissing ( const std : : string & cache_key , std : : string & & url ) {
if ( g_iconCache . MarkPending ( cache_key ) ) {
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Downloading image: %s (%s) " , url . c_str ( ) , cache_key . c_str ( ) ) ;
2023-07-21 20:04:05 +00:00
g_DownloadManager . StartDownloadWithCallback ( url , Path ( ) , http : : ProgressBarMode : : NONE , [ cache_key ] ( http : : Request & download ) {
2023-07-02 22:21:17 +00:00
if ( download . ResultCode ( ) ! = 200 )
return ;
std : : string data ;
download . buffer ( ) . TakeAll ( & data ) ;
g_iconCache . InsertIcon ( cache_key , IconFormat : : PNG , std : : move ( data ) ) ;
} ) ;
2023-06-27 21:31:15 +00:00
}
2023-06-15 20:17:27 +00:00
}
2023-06-27 21:31:15 +00:00
Statistics GetStatistics ( ) {
return g_stats ;
2023-06-15 20:17:27 +00:00
}
2023-06-27 21:31:15 +00:00
std : : string GetGameAchievementSummary ( ) {
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
2023-06-15 20:17:27 +00:00
2023-06-27 21:31:15 +00:00
rc_client_user_game_summary_t summary ;
rc_client_get_user_game_summary ( g_rcClient , & summary ) ;
2023-07-14 13:24:58 +00:00
std : : string summaryString ;
if ( summary . num_core_achievements + summary . num_unofficial_achievements = = 0 ) {
summaryString = ac - > T ( " This game has no achievements " ) ;
} else {
2023-10-04 12:36:42 +00:00
summaryString = ApplySafeSubstitutions ( ac - > T ( " Earned " , " You have unlocked %1 of %2 achievements, earning %3 of %4 points " ) ,
2023-07-14 13:24:58 +00:00
summary . num_unlocked_achievements , summary . num_core_achievements + summary . num_unofficial_achievements ,
summary . points_unlocked , summary . points_core ) ;
2023-12-03 15:41:29 +00:00
if ( HardcoreModeActive ( ) ) {
2023-07-14 13:24:58 +00:00
summaryString . append ( " \n " ) ;
2023-11-30 16:33:14 +00:00
summaryString . append ( ac - > T ( " Hardcore Mode " ) ) ;
2023-07-14 13:24:58 +00:00
}
if ( EncoreModeActive ( ) ) {
summaryString . append ( " \n " ) ;
summaryString . append ( ac - > T ( " Encore Mode " ) ) ;
}
if ( UnofficialEnabled ( ) ) {
summaryString . append ( " \n " ) ;
summaryString . append ( ac - > T ( " Unofficial achievements " ) ) ;
}
2023-06-15 20:17:27 +00:00
}
2023-06-27 21:31:15 +00:00
return summaryString ;
}
2023-06-15 20:17:27 +00:00
2023-08-28 10:15:24 +00:00
// Can happen two ways.
void ShowNotLoggedInMessage ( ) {
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
g_OSD . Show ( OSDType : : MESSAGE_ERROR , ac - > T ( " Failed to connect to RetroAchievements. Achievements will not unlock. " ) , " " , g_RAImageID , 6.0f ) ;
}
2023-06-27 21:31:15 +00:00
void identify_and_load_callback ( int result , const char * error_message , rc_client_t * client , void * userdata ) {
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
2023-06-15 20:17:27 +00:00
2024-07-14 12:42:59 +00:00
NOTICE_LOG ( Log : : Achievements , " Load callback: %d (%s) " , result , error_message ) ;
2023-06-15 20:17:27 +00:00
2023-06-27 21:31:15 +00:00
switch ( result ) {
case RC_OK :
{
// Successful! Show a message that we're active.
const rc_client_game_t * gameInfo = rc_client_get_game_info ( client ) ;
2023-06-15 20:17:27 +00:00
2023-07-02 22:14:23 +00:00
char cacheId [ 128 ] ;
snprintf ( cacheId , sizeof ( cacheId ) , " gi:%s " , gameInfo - > badge_name ) ;
2023-06-27 21:31:15 +00:00
char temp [ 512 ] ;
if ( RC_OK = = rc_client_game_get_image_url ( gameInfo , temp , sizeof ( temp ) ) ) {
2023-08-18 10:48:57 +00:00
Achievements : : DownloadImageIfMissing ( cacheId , std : : string ( temp ) ) ;
2023-06-27 21:31:15 +00:00
}
2023-07-03 12:39:49 +00:00
g_OSD . Show ( OSDType : : MESSAGE_INFO , std : : string ( gameInfo - > title ) , GetGameAchievementSummary ( ) , cacheId , 5.0f ) ;
2023-06-27 21:31:15 +00:00
break ;
}
case RC_NO_GAME_LOADED :
// The current game does not support achievements.
2023-08-25 14:36:29 +00:00
g_OSD . Show ( OSDType : : MESSAGE_INFO , ac - > T ( " RetroAchievements are not available for this game " ) , " " , g_RAImageID , 3.0f ) ;
2023-06-27 21:31:15 +00:00
break ;
2023-08-28 10:15:24 +00:00
case RC_NO_RESPONSE :
// We lost the internet connection at some point and can't report achievements.
ShowNotLoggedInMessage ( ) ;
break ;
2023-06-27 21:31:15 +00:00
default :
// Other various errors.
2024-07-14 12:42:59 +00:00
ERROR_LOG ( Log : : Achievements , " Failed to identify/load game: %d (%s) " , result , error_message ) ;
2023-08-28 10:15:24 +00:00
g_OSD . Show ( OSDType : : MESSAGE_ERROR , ac - > T ( " Failed to identify game. Achievements will not unlock. " ) , " " , g_RAImageID , 6.0f ) ;
2023-06-27 21:31:15 +00:00
break ;
}
2023-07-03 07:18:25 +00:00
g_isIdentifying = false ;
2023-06-15 20:17:27 +00:00
}
2023-07-23 21:40:53 +00:00
bool IsReadyToStart ( ) {
return ! g_isLoggingIn ;
}
2023-09-06 08:56:51 +00:00
void SetGame ( const Path & path , IdentifiedFileType fileType , FileLoader * fileLoader ) {
2024-04-29 18:32:53 +00:00
bool homebrew = false ;
2023-09-06 08:56:51 +00:00
switch ( fileType ) {
case IdentifiedFileType : : PSP_ISO :
case IdentifiedFileType : : PSP_ISO_NP :
// These file types are OK.
break ;
2024-04-29 18:32:53 +00:00
case IdentifiedFileType : : PSP_PBP_DIRECTORY :
// This should be a homebrew, which we now support as well.
// We select the homebrew hashing method.
homebrew = true ;
break ;
2023-09-06 08:56:51 +00:00
default :
// Other file types are not yet supported.
// TODO: Should we show an OSD popup here?
2024-07-14 12:42:59 +00:00
WARN_LOG ( Log : : Achievements , " File type of '%s' is not yet compatible with RetroAchievements " , path . c_str ( ) ) ;
2023-09-06 08:56:51 +00:00
return ;
}
2023-07-13 10:10:20 +00:00
if ( g_isLoggingIn ) {
2023-08-28 10:15:24 +00:00
// IsReadyToStart should have been checked the same frame, so we shouldn't be here.
// Maybe there's a race condition possible, but don't think so.
2024-07-14 12:42:59 +00:00
ERROR_LOG ( Log : : Achievements , " Still logging in during SetGame - shouldn't happen " ) ;
2023-07-13 10:10:20 +00:00
}
2023-06-27 21:31:15 +00:00
if ( ! g_rcClient | | ! IsLoggedIn ( ) ) {
2023-08-28 12:38:32 +00:00
if ( g_Config . bAchievementsEnable & & HasToken ( ) ) {
2023-08-28 10:15:24 +00:00
ShowNotLoggedInMessage ( ) ;
2023-08-25 13:55:31 +00:00
}
2023-06-27 21:31:15 +00:00
// Nothing to do.
return ;
2023-06-15 14:40:30 +00:00
}
2023-06-15 11:40:37 +00:00
2024-04-03 14:21:31 +00:00
// The caller should hold off on executing game code until this turns false, checking with IsBlockingExecution()
2024-04-05 09:07:57 +00:00
g_gamePath = path ;
2024-04-03 14:21:31 +00:00
g_isIdentifying = true ;
2023-07-12 13:37:28 +00:00
2024-04-29 18:32:53 +00:00
if ( homebrew ) {
// Homebrew hashing method - just hash the eboot.
s_game_hash = ComputePSPHomebrewHash ( fileLoader ) ;
} else {
// ISO hashing method.
//
// TODO: Fish the block device out of the loading process somewhere else. Though, probably easier to just do it here,
// we need a temporary blockdevice anyway since it gets consumed by ComputePSPISOHash.
2024-04-03 14:21:31 +00:00
BlockDevice * blockDevice ( constructBlockDevice ( fileLoader ) ) ;
if ( ! blockDevice ) {
2024-07-14 12:42:59 +00:00
ERROR_LOG ( Log : : Achievements , " Failed to construct block device for '%s' - can't identify " , path . c_str ( ) ) ;
2024-04-03 14:21:31 +00:00
g_isIdentifying = false ;
return ;
}
2023-07-11 23:11:09 +00:00
2024-04-03 14:21:31 +00:00
// This consumes the blockDevice.
2024-04-29 18:32:53 +00:00
s_game_hash = ComputePSPISOHash ( blockDevice ) ;
2024-04-03 14:21:31 +00:00
if ( ! s_game_hash . empty ( ) ) {
2024-07-14 12:42:59 +00:00
INFO_LOG ( Log : : Achievements , " Hash: %s " , s_game_hash . c_str ( ) ) ;
2024-04-03 14:21:31 +00:00
}
}
2023-07-03 07:18:25 +00:00
2023-07-02 22:14:23 +00:00
// Apply pre-load settings.
2024-04-05 20:41:08 +00:00
rc_client_set_hardcore_enabled ( g_rcClient , g_Config . bAchievementsHardcoreMode ? 1 : 0 ) ;
2023-07-02 22:14:23 +00:00
rc_client_set_encore_mode_enabled ( g_rcClient , g_Config . bAchievementsEncoreMode ? 1 : 0 ) ;
2023-07-03 20:17:07 +00:00
rc_client_set_unofficial_enabled ( g_rcClient , g_Config . bAchievementsUnofficial ? 1 : 0 ) ;
2023-07-02 22:14:23 +00:00
2024-04-03 14:21:31 +00:00
rc_client_begin_load_game ( g_rcClient , s_game_hash . c_str ( ) , & identify_and_load_callback , nullptr ) ;
2023-06-15 20:17:27 +00:00
}
2023-06-27 21:31:15 +00:00
void UnloadGame ( ) {
if ( g_rcClient ) {
rc_client_unload_game ( g_rcClient ) ;
2024-04-05 09:07:57 +00:00
g_gamePath . clear ( ) ;
s_game_hash . clear ( ) ;
2023-06-15 20:17:27 +00:00
}
}
2023-06-27 21:31:15 +00:00
void change_media_callback ( int result , const char * error_message , rc_client_t * client , void * userdata ) {
2023-09-12 12:17:50 +00:00
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
2024-07-14 12:42:59 +00:00
NOTICE_LOG ( Log : : Achievements , " Change media callback: %d (%s) " , result , error_message ) ;
2023-07-03 13:12:30 +00:00
g_isIdentifying = false ;
2023-09-12 12:17:50 +00:00
switch ( result ) {
case RC_OK :
{
// Successful! Later, show a message that we succeeded.
break ;
}
case RC_NO_GAME_LOADED :
// The current game does not support achievements.
g_OSD . Show ( OSDType : : MESSAGE_INFO , ac - > T ( " RetroAchievements are not available for this game " ) , " " , g_RAImageID , 3.0f ) ;
break ;
case RC_NO_RESPONSE :
// We lost the internet connection at some point and can't report achievements.
ShowNotLoggedInMessage ( ) ;
break ;
default :
// Other various errors.
2024-07-14 12:42:59 +00:00
ERROR_LOG ( Log : : Achievements , " Failed to identify/load game: %d (%s) " , result , error_message ) ;
2023-09-12 12:17:50 +00:00
g_OSD . Show ( OSDType : : MESSAGE_ERROR , ac - > T ( " Failed to identify game. Achievements will not unlock. " ) , " " , g_RAImageID , 6.0f ) ;
break ;
}
2023-06-15 20:17:27 +00:00
}
2023-09-12 12:17:50 +00:00
void ChangeUMD ( const Path & path , FileLoader * fileLoader ) {
2023-07-03 13:12:30 +00:00
if ( ! IsActive ( ) ) {
// Nothing to do.
return ;
}
2023-06-15 20:17:27 +00:00
2024-04-03 14:21:31 +00:00
BlockDevice * blockDevice = constructBlockDevice ( fileLoader ) ;
if ( ! blockDevice ) {
2024-07-14 12:42:59 +00:00
ERROR_LOG ( Log : : Achievements , " Failed to construct block device for '%s' - can't identify " , path . c_str ( ) ) ;
2023-09-12 12:17:50 +00:00
return ;
}
g_isIdentifying = true ;
2024-04-03 14:21:31 +00:00
// This consumes the blockDevice.
2024-04-29 18:32:53 +00:00
s_game_hash = ComputePSPISOHash ( blockDevice ) ;
2024-04-03 14:21:31 +00:00
if ( s_game_hash . empty ( ) ) {
2024-07-14 12:42:59 +00:00
ERROR_LOG ( Log : : Achievements , " Failed to hash - can't identify " ) ;
2024-04-03 14:21:31 +00:00
return ;
}
rc_client_begin_change_media_from_hash ( g_rcClient ,
s_game_hash . c_str ( ) ,
2023-06-27 21:31:15 +00:00
& change_media_callback ,
nullptr
) ;
2023-06-15 20:17:27 +00:00
}
2023-07-10 08:39:44 +00:00
std : : set < uint32_t > GetActiveChallengeIDs ( ) {
return g_activeChallenges ;
}
2023-06-27 21:31:15 +00:00
} // namespace Achievements