2023-06-18 14:24:47 +02: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.
2023-06-15 22:17:27 +02:00
# include <algorithm>
# include <atomic>
# include <cstdarg>
# include <cstdlib>
# include <ctime>
# include <functional>
2023-07-10 10:39:44 +02:00
# include <set>
2023-06-15 22:17:27 +02:00
# include <string>
# include <vector>
2023-06-16 10:31:16 +02:00
# include <mutex>
2023-06-15 22:17:27 +02:00
2023-06-15 13:40:37 +02:00
# include "ext/rcheevos/include/rcheevos.h"
2023-06-27 23:31:15 +02:00
# include "ext/rcheevos/include/rc_client.h"
2023-06-15 16:40:30 +02:00
# include "ext/rcheevos/include/rc_api_user.h"
2023-06-15 22:17:27 +02: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-17 01:52:28 +02:00
# include "ext/rcheevos/include/rc_hash.h"
# include "ext/rcheevos/src/rhash/md5.h"
2023-06-15 13:40:37 +02:00
2023-06-15 22:17:27 +02:00
# include "ext/rapidjson/include/rapidjson/document.h"
# include "Common/Log.h"
# include "Common/File/Path.h"
2023-06-16 10:31:16 +02:00
# include "Common/File/FileUtil.h"
2023-07-12 01:11:09 +02:00
# include "Core/FileLoaders/LocalFileLoader.h"
# include "Core/FileSystems/BlockDevices.h"
2023-06-15 22:17:27 +02:00
# include "Common/Net/HTTPClient.h"
2023-06-30 17:15:49 +02:00
# include "Common/System/OSD.h"
# include "Common/System/System.h"
2023-06-26 10:01:20 +02:00
# include "Common/System/NativeApp.h"
2023-06-15 23:27:38 +02:00
# include "Common/TimeUtil.h"
# include "Common/Data/Text/I18n.h"
# include "Common/Serialize/Serializer.h"
2023-06-27 23:31:15 +02:00
# include "Common/Serialize/SerializeFuncs.h"
2023-06-27 18:00:50 +02:00
# include "Common/StringUtils.h"
2023-06-17 00:00:57 +02:00
# include "Common/Crypto/md5.h"
2023-06-18 14:43:38 +02:00
# include "Common/UI/IconCache.h"
2023-06-15 23:27:38 +02:00
# include "Core/MemMap.h"
# include "Core/Config.h"
2023-06-16 10:31:16 +02:00
# include "Core/CoreParameter.h"
# include "Core/ELF/ParamSFO.h"
# include "Core/System.h"
2023-06-17 00:00:57 +02:00
# include "Core/FileSystems/MetaFileSystem.h"
2023-07-02 12:00:13 +02:00
# include "Core/RetroAchievements.h"
2023-06-16 10:31:16 +02:00
2023-07-10 19:26:41 +02:00
static inline const char * DeNull ( const char * ptr ) {
return ptr ? ptr : " " ;
}
2023-06-16 13:04:20 +02:00
void OSDOpenBackgroundProgressDialog ( const char * str_id , std : : string message , s32 min , s32 max , s32 value ) {
NOTICE_LOG ( ACHIEVEMENTS , " Progress dialog opened: %s %s " , str_id , message . c_str ( ) ) ;
2023-06-20 22:39:22 +02:00
g_OSD . SetProgressBar ( str_id , std : : move ( message ) , min , max , value ) ;
2023-06-16 13:04:20 +02:00
}
2023-06-20 22:39:22 +02:00
2023-06-16 13:04:20 +02:00
void OSDUpdateBackgroundProgressDialog ( const char * str_id , std : : string message , s32 min , s32 max , s32 value ) {
2023-06-17 13:48:49 +02:00
NOTICE_LOG ( ACHIEVEMENTS , " Progress dialog updated: %s %s %d/(%d->%d) " , str_id , message . c_str ( ) , value , min , max ) ;
2023-06-20 22:39:22 +02:00
g_OSD . SetProgressBar ( str_id , std : : move ( message ) , min , max , value ) ;
2023-06-16 13:04:20 +02:00
}
2023-06-20 22:39:22 +02:00
2023-06-16 13:04:20 +02:00
void OSDCloseBackgroundProgressDialog ( const char * str_id ) {
NOTICE_LOG ( ACHIEVEMENTS , " Progress dialog closed: %s " , str_id ) ;
2023-07-03 14:39:49 +02:00
g_OSD . RemoveProgressBar ( str_id ) ;
2023-06-27 23:31:15 +02:00
}
2023-06-16 16:29:44 +02:00
void OnAchievementsLoginStateChange ( ) {
System_PostUIMessage ( " achievements_loginstatechange " , " " ) ;
}
2023-06-15 23:27:38 +02:00
2023-06-15 22:17:27 +02:00
namespace Achievements {
2023-06-26 10:01:20 +02:00
// It's the name of the secret, not a secret name - the value is not secret :)
2023-06-26 17:21:39 +02:00
static const char * RA_TOKEN_SECRET_NAME = " retroachievements " ;
2023-06-26 10:01:20 +02:00
2023-06-27 09:47:35 +02:00
static Achievements : : Statistics g_stats ;
2023-06-20 23:16:12 +02:00
2023-06-18 14:43:38 +02:00
const std : : string g_gameIconCachePrefix = " game: " ;
const std : : string g_iconCachePrefix = " badge: " ;
2023-06-27 23:31:15 +02:00
Path s_game_path ;
std : : string s_game_hash ;
2023-06-27 09:47:35 +02:00
2023-07-10 10:39:44 +02:00
std : : set < uint32_t > g_activeChallenges ;
2023-07-03 09:18:25 +02:00
bool g_isIdentifying = false ;
2023-06-27 23:31:15 +02:00
// rc_client implementation
static rc_client_t * g_rcClient ;
2023-06-15 13:40:37 +02:00
2023-06-27 23:31:15 +02:00
# define PSP_MEMORY_OFFSET 0x08000000
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02:00
rc_client_t * GetClient ( ) {
return g_rcClient ;
2023-06-15 16:40:30 +02:00
}
2023-06-27 23:31:15 +02:00
bool IsLoggedIn ( ) {
return rc_client_get_user_info ( g_rcClient ) ! = nullptr ;
}
2023-06-15 22:17:27 +02:00
2023-07-03 00:14:23 +02:00
bool EncoreModeActive ( ) {
if ( ! g_rcClient ) {
return false ;
}
return rc_client_get_encore_mode_enabled ( g_rcClient ) ;
}
2023-07-03 22:17:07 +02:00
bool UnofficialEnabled ( ) {
if ( ! g_rcClient ) {
return false ;
}
return rc_client_get_unofficial_enabled ( g_rcClient ) ;
}
2023-07-03 00:47:54 +02:00
bool ChallengeModeActive ( ) {
if ( ! g_rcClient ) {
return false ;
}
2023-07-11 10:05:06 +02:00
return IsLoggedIn ( ) & & rc_client_get_hardcore_enabled ( g_rcClient ) ;
2023-07-03 00:47:54 +02:00
}
bool WarnUserIfChallengeModeActive ( const char * message ) {
if ( ! ChallengeModeActive ( ) ) {
return false ;
}
const char * showMessage = message ;
if ( ! message ) {
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
showMessage = ac - > T ( " This feature is not available in Challenge Mode " ) ;
}
g_OSD . Show ( OSDType : : MESSAGE_WARNING , showMessage , 3.0f ) ;
return true ;
2023-06-27 14:58:39 +02:00
}
2023-07-03 09:18:25 +02:00
bool IsBlockingExecution ( ) {
return g_isIdentifying ;
}
2023-07-03 00:47:54 +02:00
static u32 GetGameID ( ) {
2023-06-27 23:31:15 +02:00
if ( ! g_rcClient ) {
return 0 ;
2023-06-15 22:17:27 +02:00
}
2023-06-15 16:40:30 +02:00
2023-06-27 23:31:15 +02:00
const rc_client_game_t * info = rc_client_get_game_info ( g_rcClient ) ;
if ( ! info ) {
return 0 ;
}
2023-07-03 00:14:23 +02:00
return info - > id ; // 0 if not identified
2023-06-15 22:17:27 +02:00
}
2023-06-15 16:40:30 +02:00
2023-07-03 00:47:54 +02:00
bool IsActive ( ) {
return GetGameID ( ) ! = 0 ;
}
2023-06-27 23:31:15 +02:00
// This is the function the rc_client will use to read memory for the emulator. we don't need it yet,
// so just provide a dummy function that returns "no memory read".
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 16:11:02 +02:00
uint32_t orig_address = address ;
2023-06-27 23:31:15 +02:00
address + = PSP_MEMORY_OFFSET ;
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02:00
if ( ! Memory : : IsValidAddress ( address ) ) {
// Some achievement packs are really, really spammy.
// So we'll just count the bad accesses.
Achievements : : g_stats . badMemoryAccessCount + + ;
if ( g_Config . bAchievementsLogBadMemReads ) {
2023-07-10 16:11:02 +02:00
WARN_LOG ( G3D , " RetroAchievements PeekMemory: Bad address %08x (%d bytes) (%08x was passed in) " , address , num_bytes , orig_address ) ;
2023-06-15 22:17:27 +02:00
}
2023-07-10 16:11:02 +02:00
// TEMPORARY HACK: rcheevos' handling of bad memory accesses causes a LOT of extra work, since
// for some reason these invalid accesses keeps happening. So we'll temporarily to back to the previous
// behavior of simply returning 0.
uint32_t temp = 0 ;
memcpy ( buffer , & temp , num_bytes ) ;
return num_bytes ;
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
switch ( num_bytes ) {
case 1 :
* buffer = Memory : : ReadUnchecked_U8 ( address ) ;
return 1 ;
case 2 : {
uint16_t temp = Memory : : ReadUnchecked_U16 ( address ) ;
memcpy ( buffer , & temp , 2 ) ;
return 2 ;
}
case 4 : {
uint32_t temp = Memory : : ReadUnchecked_U32 ( address ) ;
memcpy ( buffer , & temp , 4 ) ;
return 4 ;
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
default :
return 0 ;
2023-06-15 22:17:27 +02:00
}
}
2023-06-27 23:31:15 +02: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.
if ( request - > post_data ) {
g_DownloadManager . AsyncPostWithCallback ( std : : string ( request - > url ) , std : : string ( request - > post_data ) , " application/x-www-form-urlencoded " , [ = ] ( http : : Download & download ) {
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 ) ;
} ) ;
} else {
g_DownloadManager . StartDownloadWithCallback ( std : : string ( request - > url ) , Path ( ) , [ = ] ( http : : Download & download ) {
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 ) ;
} ) ;
}
}
// Write log messages to the console
static void log_message_callback ( const char * message , const rc_client_t * client ) {
INFO_LOG ( ACHIEVEMENTS , " RetroAchievements log: %s " , message ) ;
}
static void login_token_callback ( int result , const char * error_message , rc_client_t * client , void * userdata ) {
switch ( result ) {
case RC_OK :
2023-07-03 00:14:23 +02:00
OnAchievementsLoginStateChange ( ) ;
2023-06-27 23:31:15 +02:00
break ;
case RC_INVALID_STATE :
case RC_API_FAILURE :
case RC_MISSING_VALUE :
case RC_INVALID_JSON :
ERROR_LOG ( ACHIEVEMENTS , " Failure logging in via token: %d, %s " , result , error_message ) ;
2023-07-03 00:14:23 +02:00
OnAchievementsLoginStateChange ( ) ;
2023-06-27 23:31:15 +02:00
break ;
}
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02: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 14:39:49 +02:00
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
2023-06-27 23:31:15 +02: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-03 00:14:23 +02:00
g_OSD . ShowAchievementUnlocked ( event - > achievement - > id ) ;
2023-07-03 14:39:49 +02:00
INFO_LOG ( ACHIEVEMENTS , " Achievement unlocked: '%s' (%d) " , event - > achievement - > title , event - > achievement - > id ) ;
2023-06-27 23:31:15 +02:00
break ;
case RC_CLIENT_EVENT_GAME_COMPLETED :
2023-07-03 14:39:49 +02:00
{
2023-06-27 23:31:15 +02:00
// All achievements for the game have been earned. The handler should notify the player that the game was completed or mastered, depending on challenge mode.
2023-07-03 14:39:49 +02:00
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
const rc_client_game_t * gameInfo = rc_client_get_game_info ( g_rcClient ) ;
// TODO: Translation?
std : : string title = ReplaceAll ( ac - > T ( " Mastered %1 " ) , " %1 " , gameInfo - > title ) ;
rc_client_user_game_summary_t summary ;
rc_client_get_user_game_summary ( g_rcClient , & summary ) ;
std : : string message = StringFromFormat ( ac - > T ( " %d achievements " ) , summary . num_unlocked_achievements ) ;
2023-07-10 19:26:41 +02:00
g_OSD . Show ( OSDType : : MESSAGE_INFO , title , message , DeNull ( gameInfo - > badge_name ) , 10.0f ) ;
2023-07-03 14:39:49 +02:00
INFO_LOG ( ACHIEVEMENTS , " %s " , message . c_str ( ) ) ;
2023-06-27 23:31:15 +02:00
break ;
2023-07-03 14:39:49 +02:00
}
2023-06-27 23:31:15 +02:00
case 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.
2023-07-03 14:39:49 +02:00
INFO_LOG ( ACHIEVEMENTS , " Leaderboard attempt started: %s " , event - > leaderboard - > title ) ;
2023-07-10 19:26:41 +02:00
g_OSD . Show ( OSDType : : MESSAGE_INFO , ReplaceAll ( ac - > T ( " %1: Leaderboard attempt started " ) , " %1 " , event - > leaderboard - > title ) , DeNull ( event - > leaderboard - > description ) , 3.0f ) ;
2023-06-27 23:31:15 +02:00
break ;
case RC_CLIENT_EVENT_LEADERBOARD_FAILED :
NOTICE_LOG ( ACHIEVEMENTS , " Leaderboard attempt failed: %s " , event - > leaderboard - > title ) ;
2023-07-03 00:47:54 +02:00
g_OSD . Show ( OSDType : : MESSAGE_INFO , ReplaceAll ( ac - > T ( " %1: Leaderboard attempt failed " ) , " %1 " , event - > leaderboard - > title ) , 3.0f ) ;
2023-06-27 23:31:15 +02:00
// A leaderboard attempt has failed.
break ;
case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED :
NOTICE_LOG ( ACHIEVEMENTS , " Leaderboard result submitted: %s " , event - > leaderboard - > title ) ;
2023-07-10 19:26:41 +02:00
g_OSD . Show ( OSDType : : MESSAGE_SUCCESS , ReplaceAll ( ReplaceAll ( ac - > T ( " %1: Submitting leaderboard score: %2! " ) , " %1 " , DeNull ( event - > leaderboard - > title ) ) , " %2 " , DeNull ( event - > leaderboard - > tracker_value ) ) , DeNull ( event - > leaderboard - > description ) , 3.0f ) ;
2023-06-27 23:31:15 +02:00
// A leaderboard attempt was completed.The handler may show a message with the leaderboard title and /or description indicating the final value being submitted to the server.
break ;
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW :
NOTICE_LOG ( ACHIEVEMENTS , " Challenge indicator show: %s " , event - > achievement - > title ) ;
2023-07-03 14:39:49 +02:00
g_OSD . ShowChallengeIndicator ( event - > achievement - > id , true ) ;
2023-07-10 10:39:44 +02:00
g_activeChallenges . insert ( event - > achievement - > id ) ;
2023-06-27 23:31:15 +02: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 :
NOTICE_LOG ( ACHIEVEMENTS , " Challenge indicator hide: %s " , event - > achievement - > title ) ;
2023-07-03 14:39:49 +02:00
g_OSD . ShowChallengeIndicator ( event - > achievement - > id , false ) ;
2023-07-10 10:39:44 +02:00
g_activeChallenges . erase ( event - > achievement - > id ) ;
2023-06-27 23:31:15 +02: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 :
2023-07-04 00:49:17 +02:00
NOTICE_LOG ( ACHIEVEMENTS , " Progress indicator show: %s, progress: '%s' (%f) " , event - > achievement - > title , event - > achievement - > measured_progress , event - > achievement - > measured_percent ) ;
2023-06-27 23:31:15 +02: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 ( event - > achievement - > id , 2.0f ) ;
break ;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW :
2023-07-03 00:47:54 +02:00
NOTICE_LOG ( ACHIEVEMENTS , " Leaderboard tracker show: '%s' (id %d) " , event - > leaderboard_tracker - > display , event - > leaderboard_tracker - > id ) ;
2023-06-27 23:31:15 +02: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 14:39:49 +02:00
g_OSD . ShowLeaderboardTracker ( event - > leaderboard_tracker - > id , event - > leaderboard_tracker - > display , true ) ;
2023-06-27 23:31:15 +02:00
break ;
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE :
2023-07-03 14:39:49 +02:00
// A leaderboard_tracker has become inactive. The handler should hide the tracker text from the screen.
2023-07-03 00:47:54 +02:00
NOTICE_LOG ( ACHIEVEMENTS , " Leaderboard tracker hide: '%s' (id %d) " , event - > leaderboard_tracker - > display , event - > leaderboard_tracker - > id ) ;
2023-07-03 14:39:49 +02:00
g_OSD . ShowLeaderboardTracker ( event - > leaderboard_tracker - > id , nullptr , false ) ;
2023-06-27 23:31:15 +02: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.
2023-07-03 00:47:54 +02:00
NOTICE_LOG ( ACHIEVEMENTS , " Leaderboard tracker update: '%s' (id %d) " , event - > leaderboard_tracker - > display , event - > leaderboard_tracker - > id ) ;
g_OSD . ShowLeaderboardTracker ( event - > leaderboard_tracker - > id , event - > leaderboard_tracker - > display , true ) ;
2023-06-27 23:31:15 +02:00
break ;
case RC_CLIENT_EVENT_RESET :
2023-07-03 14:39:49 +02:00
WARN_LOG ( ACHIEVEMENTS , " Resetting game due to achievement setting change! " ) ;
2023-06-27 23:31:15 +02:00
// Challenge mode was enabled, or something else that forces a game reset.
System_PostUIMessage ( " reset " , " " ) ;
break ;
case RC_CLIENT_EVENT_SERVER_ERROR :
ERROR_LOG ( ACHIEVEMENTS , " Server error: %s: %s " , event - > server_error - > api , event - > server_error - > error_message ) ;
2023-07-03 14:39:49 +02:00
g_OSD . Show ( OSDType : : MESSAGE_ERROR , " Server error " ) ;
2023-06-27 23:31:15 +02:00
break ;
default :
WARN_LOG ( ACHIEVEMENTS , " Unhandled rc_client event %d, ignoring " , event - > type ) ;
break ;
}
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
void Initialize ( ) {
2023-07-01 12:31:46 +02:00
if ( ! g_Config . bAchievementsEnable ) {
_dbg_assert_ ( ! g_rcClient ) ;
INFO_LOG ( ACHIEVEMENTS , " Achievements are disabled, not initializing. " ) ;
return ;
}
2023-07-11 10:16:58 +02:00
_assert_msg_ ( ! g_rcClient , " Achievements already initialized " ) ;
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02:00
g_rcClient = rc_client_create ( read_memory_callback , server_call_callback ) ;
2023-07-11 10:16:58 +02:00
if ( ! g_rcClient ) {
// Shouldn't happen really.
return ;
}
2023-06-27 23:31:15 +02:00
// Provide a logging function to simplify debugging
rc_client_enable_logging ( g_rcClient , RC_CLIENT_LOG_LEVEL_VERBOSE , log_message_callback ) ;
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02:00
// Disable SSL for now.
rc_client_set_host ( g_rcClient , " http://retroachievements.org " ) ;
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02:00
rc_client_set_event_handler ( g_rcClient , event_handler_callback ) ;
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02:00
std : : string api_token = NativeLoadSecret ( RA_TOKEN_SECRET_NAME ) ;
if ( ! api_token . empty ( ) ) {
rc_client_begin_login_with_token ( g_rcClient , g_Config . sAchievementsUserName . c_str ( ) , api_token . c_str ( ) , & login_token_callback , nullptr ) ;
2023-06-15 22:17:27 +02:00
}
2023-07-11 10:16:58 +02:00
INFO_LOG ( ACHIEVEMENTS , " Achievements initialized. " ) ;
2023-06-27 23:31:15 +02:00
}
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02:00
static void login_password_callback ( int result , const char * error_message , rc_client_t * client , void * userdata ) {
switch ( result ) {
case RC_OK :
2023-06-15 22:17:27 +02:00
{
2023-06-27 23:31:15 +02: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-03 00:14:23 +02:00
OnAchievementsLoginStateChange ( ) ;
2023-06-27 23:31:15 +02:00
break ;
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
case RC_INVALID_STATE :
case RC_API_FAILURE :
case RC_MISSING_VALUE :
case RC_INVALID_JSON :
ERROR_LOG ( ACHIEVEMENTS , " Failure logging in via token: %d, %s " , result , error_message ) ;
2023-07-03 00:14:23 +02:00
OnAchievementsLoginStateChange ( ) ;
2023-06-27 23:31:15 +02:00
break ;
2023-06-15 22:17:27 +02:00
}
2023-06-15 23:27:38 +02:00
2023-06-27 23:31:15 +02:00
OSDCloseBackgroundProgressDialog ( " cheevos_async_login " ) ;
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
bool LoginAsync ( const char * username , const char * password ) {
if ( IsLoggedIn ( ) | | std : : strlen ( username ) = = 0 | | std : : strlen ( password ) = = 0 | | IsUsingRAIntegration ( ) )
2023-06-15 22:17:27 +02:00
return false ;
2023-06-27 23:31:15 +02:00
OSDOpenBackgroundProgressDialog ( " cheevos_async_login " , " Logging in to RetroAchivements... " , 0 , 0 , 0 ) ;
rc_client_begin_login_with_password ( g_rcClient , username , password , & login_password_callback , nullptr ) ;
2023-06-15 22:17:27 +02:00
return true ;
}
2023-06-27 23:31:15 +02:00
void Logout ( ) {
rc_client_logout ( g_rcClient ) ;
// remove from config
g_Config . sAchievementsUserName . clear ( ) ;
NativeSaveSecret ( RA_TOKEN_SECRET_NAME , " " ) ;
g_Config . Save ( " Achievements logout " ) ;
2023-07-10 10:39:44 +02:00
g_activeChallenges . clear ( ) ;
2023-07-11 10:05:55 +02:00
OnAchievementsLoginStateChange ( ) ;
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
void UpdateSettings ( ) {
2023-07-11 10:16:58 +02:00
if ( g_rcClient & & ! g_Config . bAchievementsEnable ) {
2023-06-27 23:31:15 +02:00
// we're done here
Shutdown ( ) ;
2023-06-15 22:17:27 +02:00
return ;
}
2023-07-11 10:16:58 +02:00
if ( ! g_rcClient & & g_Config . bAchievementsEnable ) {
2023-07-02 17:12:46 +02:00
// we just got enabled.
2023-06-27 23:31:15 +02:00
Initialize ( ) ;
2023-06-15 22:17:27 +02:00
}
}
2023-06-27 23:31:15 +02:00
bool Shutdown ( ) {
2023-07-10 10:39:44 +02:00
g_activeChallenges . clear ( ) ;
2023-06-27 23:31:15 +02:00
rc_client_destroy ( g_rcClient ) ;
g_rcClient = nullptr ;
2023-07-11 10:16:58 +02:00
INFO_LOG ( ACHIEVEMENTS , " Achievements shut down. " ) ;
2023-06-15 22:17:27 +02:00
return true ;
}
2023-06-27 23:31:15 +02:00
void ResetRuntime ( ) {
2023-06-23 22:08:52 +02:00
INFO_LOG ( ACHIEVEMENTS , " Resetting rcheevos state... " ) ;
2023-06-27 23:31:15 +02:00
rc_client_reset ( g_rcClient ) ;
2023-07-10 10:39:44 +02:00
g_activeChallenges . clear ( ) ;
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
void FrameUpdate ( ) {
if ( ! g_rcClient )
2023-06-15 22:17:27 +02:00
return ;
2023-06-27 23:31:15 +02:00
rc_client_do_frame ( g_rcClient ) ;
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
void Idle ( ) {
rc_client_idle ( g_rcClient ) ;
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
void DoState ( PointerWrap & p ) {
2023-07-01 12:31:46 +02:00
auto sw = p . Section ( " Achievements " , 0 , 1 ) ;
2023-06-15 23:27:38 +02:00
if ( ! sw ) {
// Save state is missing the section.
// Reset the runtime.
2023-07-01 12:31:46 +02:00
if ( HasAchievementsOrLeaderboards ( ) ) {
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
g_OSD . Show ( OSDType : : MESSAGE_WARNING , ac - > T ( " Save state loaded without achievement data " ) , 5.0f ) ;
}
rc_client_reset ( g_rcClient ) ;
2023-06-15 23:27:38 +02:00
return ;
}
2023-06-27 23:31:15 +02:00
uint32_t data_size = 0 ;
2023-07-01 12:31:46 +02:00
2023-07-03 00:14:23 +02:00
if ( ! IsActive ( ) ) {
2023-07-01 12:31:46 +02:00
Do ( p , data_size ) ;
2023-07-03 00:14:23 +02:00
if ( p . mode = = PointerWrap : : MODE_READ ) {
WARN_LOG ( ACHIEVEMENTS , " Save state contained achievement data, but achievements are not active. Ignore. " ) ;
}
2023-07-01 12:31:46 +02:00
p . SkipBytes ( data_size ) ;
return ;
}
2023-06-27 23:31:15 +02: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 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
Do ( p , data_size ) ;
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02: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 12:31:46 +02:00
{
int retval = rc_client_serialize_progress ( g_rcClient , buffer ) ;
if ( retval ! = RC_OK ) {
ERROR_LOG ( ACHIEVEMENTS , " Error %d serializing achievement data. Ignoring. " , retval ) ;
}
2023-06-27 23:31:15 +02:00
break ;
2023-06-15 22:17:27 +02:00
}
2023-07-12 01:11:09 +02:00
default :
break ;
2023-07-01 12:31:46 +02:00
}
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02:00
DoArray ( p , buffer , data_size ) ;
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02:00
switch ( p . mode ) {
case PointerWrap : : MODE_READ :
2023-07-01 12:31:46 +02:00
{
int retval = rc_client_deserialize_progress ( g_rcClient , buffer ) ;
if ( retval ! = RC_OK ) {
// TODO: What should we really do here?
ERROR_LOG ( ACHIEVEMENTS , " Error %d deserializing achievement data. Ignoring. " , retval ) ;
}
2023-06-27 23:31:15 +02:00
break ;
2023-06-15 22:17:27 +02:00
}
2023-07-12 01:11:09 +02:00
default :
break ;
2023-07-01 12:31:46 +02:00
}
2023-06-27 23:31:15 +02:00
delete [ ] buffer ;
2023-07-01 12:31:46 +02:00
} else {
2023-07-03 00:14:23 +02:00
if ( IsActive ( ) ) {
2023-07-01 12:31:46 +02:00
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
g_OSD . Show ( OSDType : : MESSAGE_WARNING , ac - > T ( " Save state loaded without achievement data " ) , 5.0f ) ;
}
rc_client_reset ( g_rcClient ) ;
2023-06-15 22:17:27 +02:00
}
}
2023-06-27 23:31:15 +02:00
bool HasAchievementsOrLeaderboards ( ) {
if ( ! g_rcClient ) {
return false ;
}
2023-07-03 00:14:23 +02:00
return IsActive ( ) ;
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
void DownloadImageIfMissing ( const std : : string & cache_key , std : : string & & url ) {
if ( g_iconCache . MarkPending ( cache_key ) ) {
INFO_LOG ( ACHIEVEMENTS , " Downloading image: %s (%s) " , url . c_str ( ) , cache_key . c_str ( ) ) ;
2023-07-03 00:21:17 +02:00
g_DownloadManager . StartDownloadWithCallback ( url , Path ( ) , [ cache_key ] ( http : : Download & download ) {
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 23:31:15 +02:00
}
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
Statistics GetStatistics ( ) {
return g_stats ;
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
std : : string GetGameAchievementSummary ( ) {
auto ac = GetI18NCategory ( I18NCat : : ACHIEVEMENTS ) ;
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02:00
rc_client_user_game_summary_t summary ;
rc_client_get_user_game_summary ( g_rcClient , & summary ) ;
std : : string summaryString = StringFromFormat ( ac - > T ( " Earned " , " You have unlocked %d of %d achievements, earning %d of %d points " ) ,
summary . num_unlocked_achievements , summary . num_core_achievements + summary . num_unofficial_achievements ,
summary . points_unlocked , summary . points_core ) ;
if ( ChallengeModeActive ( ) ) {
summaryString . append ( " \n " ) ;
2023-07-03 00:14:23 +02:00
summaryString . append ( ac - > T ( " Challenge Mode " ) ) ;
}
if ( EncoreModeActive ( ) ) {
summaryString . append ( " \n " ) ;
summaryString . append ( ac - > T ( " Encore Mode " ) ) ;
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
return summaryString ;
}
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02: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 22:17:27 +02:00
2023-06-27 23:31:15 +02:00
NOTICE_LOG ( ACHIEVEMENTS , " Load callback: %d (%s) " , result , error_message ) ;
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02: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 22:17:27 +02:00
2023-07-03 00:14:23 +02:00
char cacheId [ 128 ] ;
snprintf ( cacheId , sizeof ( cacheId ) , " gi:%s " , gameInfo - > badge_name ) ;
2023-06-27 23:31:15 +02:00
char temp [ 512 ] ;
if ( RC_OK = = rc_client_game_get_image_url ( gameInfo , temp , sizeof ( temp ) ) ) {
2023-07-03 00:14:23 +02:00
Achievements : : DownloadImageIfMissing ( cacheId , std : : move ( std : : string ( temp ) ) ) ;
2023-06-27 23:31:15 +02:00
}
2023-07-03 14:39:49 +02:00
g_OSD . Show ( OSDType : : MESSAGE_INFO , std : : string ( gameInfo - > title ) , GetGameAchievementSummary ( ) , cacheId , 5.0f ) ;
2023-06-27 23:31:15 +02:00
break ;
}
case RC_NO_GAME_LOADED :
// The current game does not support achievements.
2023-07-11 10:30:50 +02:00
g_OSD . Show ( OSDType : : MESSAGE_INFO , ac - > T ( " RetroAchievements are not available for this game " ) , 3.0f ) ;
2023-06-27 23:31:15 +02:00
break ;
default :
// Other various errors.
ERROR_LOG ( ACHIEVEMENTS , " Failed to identify/load game: %d (%s) " , result , error_message ) ;
break ;
}
2023-07-03 09:18:25 +02:00
g_isIdentifying = false ;
2023-06-15 22:17:27 +02:00
}
2023-07-12 01:11:09 +02:00
struct FileContext {
BlockDevice * bd ;
int64_t seekPos ;
} ;
static BlockDevice * g_blockDevice ;
void SetGame ( const Path & path , FileLoader * fileLoader ) {
2023-06-27 23:31:15 +02:00
if ( ! g_rcClient | | ! IsLoggedIn ( ) ) {
// Nothing to do.
return ;
2023-06-15 16:40:30 +02:00
}
2023-06-15 13:40:37 +02:00
2023-07-12 01:11:09 +02:00
// TODO: Fish the block device out of the loading process somewhere else. Though, probably easier to just do it here.
g_blockDevice = constructBlockDevice ( fileLoader ) ;
if ( ! g_blockDevice ) {
ERROR_LOG ( ACHIEVEMENTS , " Failed to construct block device for '%s' - can't identify " , path . c_str ( ) ) ;
return ;
}
2023-06-21 15:25:17 +02:00
rc_hash_filereader rc_filereader ;
2023-06-27 23:31:15 +02:00
rc_filereader . open = [ ] ( const char * utf8Path ) {
2023-07-12 01:11:09 +02:00
return ( void * ) new FileContext { g_blockDevice , 0 } ;
} ;
rc_filereader . seek = [ ] ( void * file_handle , int64_t offset , int origin ) {
FileContext * ctx = ( FileContext * ) file_handle ;
switch ( origin ) {
case SEEK_SET : ctx - > seekPos = offset ; break ;
case SEEK_END : ctx - > seekPos = ctx - > bd - > GetBlockSize ( ) * ctx - > bd - > GetNumBlocks ( ) + offset ; break ;
case SEEK_CUR : ctx - > seekPos + = offset ; break ;
default : break ;
}
} ;
rc_filereader . tell = [ ] ( void * file_handle ) - > int64_t {
return ( ( FileContext * ) file_handle ) - > seekPos ;
} ;
rc_filereader . read = [ ] ( void * file_handle , void * buffer , size_t requested_bytes ) - > size_t {
FileContext * ctx = ( FileContext * ) file_handle ;
int blockSize = ctx - > bd - > GetBlockSize ( ) ;
int64_t offset = ctx - > seekPos ;
int64_t endOffset = ctx - > seekPos + requested_bytes ;
int firstBlock = offset / blockSize ;
int afterLastBlock = ( endOffset + blockSize - 1 ) / blockSize ;
int numBlocks = afterLastBlock - firstBlock ;
// This is suboptimal, but good enough since we're not doing a lot of accesses.
uint8_t * buf = new uint8_t [ numBlocks * blockSize ] ;
bool success = ctx - > bd - > ReadBlocks ( firstBlock , numBlocks , ( u8 * ) buf ) ;
if ( success ) {
int64_t firstOffset = firstBlock * blockSize ;
memcpy ( buffer , buf + ( offset - firstOffset ) , requested_bytes ) ;
ctx - > seekPos + = requested_bytes ;
delete [ ] buf ;
return requested_bytes ;
} else {
delete [ ] buf ;
ERROR_LOG ( ACHIEVEMENTS , " Block device load fail " ) ;
return 0 ;
}
} ;
rc_filereader . close = [ ] ( void * file_handle ) {
FileContext * ctx = ( FileContext * ) file_handle ;
delete ctx - > bd ;
delete ctx ;
2023-06-27 23:31:15 +02:00
} ;
2023-06-21 15:25:17 +02:00
2023-07-03 09:18:25 +02:00
// The caller should hold off on executing game code until this turns false, checking with IsBlockingExecution()
g_isIdentifying = true ;
2023-07-03 00:14:23 +02:00
// Apply pre-load settings.
2023-07-03 00:47:54 +02:00
rc_client_set_hardcore_enabled ( g_rcClient , g_Config . bAchievementsChallengeMode ? 1 : 0 ) ;
2023-07-03 00:14:23 +02:00
rc_client_set_encore_mode_enabled ( g_rcClient , g_Config . bAchievementsEncoreMode ? 1 : 0 ) ;
2023-07-03 22:17:07 +02:00
rc_client_set_unofficial_enabled ( g_rcClient , g_Config . bAchievementsUnofficial ? 1 : 0 ) ;
2023-07-03 00:14:23 +02:00
2023-06-21 15:25:17 +02:00
rc_hash_init_custom_filereader ( & rc_filereader ) ;
2023-06-17 01:52:28 +02:00
rc_hash_init_default_cdreader ( ) ;
2023-06-27 23:31:15 +02:00
rc_client_begin_identify_and_load_game ( g_rcClient , RC_CONSOLE_PSP , path . c_str ( ) , nullptr , 0 , & identify_and_load_callback , nullptr ) ;
2023-07-12 01:11:09 +02:00
// fclose above will have deleted it.
g_blockDevice = nullptr ;
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
void UnloadGame ( ) {
if ( g_rcClient ) {
rc_client_unload_game ( g_rcClient ) ;
2023-06-15 22:17:27 +02:00
}
}
2023-06-27 23:31:15 +02:00
void change_media_callback ( int result , const char * error_message , rc_client_t * client , void * userdata ) {
NOTICE_LOG ( ACHIEVEMENTS , " Change media callback: %d (%s) " , result , error_message ) ;
2023-07-03 15:12:30 +02:00
g_isIdentifying = false ;
2023-06-15 22:17:27 +02:00
}
2023-06-27 23:31:15 +02:00
void ChangeUMD ( const Path & path ) {
2023-07-03 15:12:30 +02:00
if ( ! IsActive ( ) ) {
// Nothing to do.
return ;
}
2023-06-15 22:17:27 +02:00
2023-06-27 23:31:15 +02:00
rc_client_begin_change_media ( g_rcClient ,
path . c_str ( ) ,
nullptr ,
0 ,
& change_media_callback ,
nullptr
) ;
2023-07-03 15:12:30 +02:00
g_isIdentifying = true ;
2023-06-15 22:17:27 +02:00
}
2023-07-10 10:39:44 +02:00
std : : set < uint32_t > GetActiveChallengeIDs ( ) {
return g_activeChallenges ;
}
2023-06-27 23:31:15 +02:00
} // namespace Achievements