@ -132,11 +132,15 @@ static void UpdateModeSettings(const Settings& old_config);
static DynamicHeapArray < u8 > SaveStateToBuffer ( ) ;
static void LoadStateFromBuffer ( std : : span < const u8 > data , std : : unique_lock < std : : recursive_mutex > & lock ) ;
static bool SaveStateToBuffer ( std : : span < u8 > data ) ;
static std : : string GetAchievementBadgeURL ( const rc_client_achievement_t * achievement , u32 image_type ) ;
static std : : string GetImageURL ( const char * image_name , u32 type ) ;
static std : : string GetLocalImagePath ( const std : : string_view image_name , u32 type ) ;
static void DownloadImage ( std : : string url , std : : string cache_path ) ;
static void PrefetchNextAchievementBadge ( ) ;
static void PrefetchNextAchievementBadge ( const rc_client_achievement_t * const last_cheevo ) ;
static void PrefetchAllAchievementBadges ( ) ;
static void SendNextPrefetchBadgeRequest ( ) ;
static void ClearPrefetchBadgeRequests ( ) ;
static TinyString DecryptLoginToken ( std : : string_view encrypted_token , std : : string_view username ) ;
static TinyString EncryptLoginToken ( std : : string_view token , std : : string_view username ) ;
@ -256,6 +260,8 @@ struct State
rc_client_all_user_progress_t * fetch_all_progress_result = nullptr ;
rc_client_async_handle_t * refresh_all_progress_request = nullptr ;
std : : vector < std : : pair < std : : string , std : : string > > prefetch_badge_requests ; // (path, url)
# ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
rc_client_async_handle_t * load_raintegration_request = nullptr ;
bool using_raintegration = false ;
@ -504,6 +510,93 @@ void Achievements::PrefetchNextAchievementBadge(const rc_client_achievement_t* c
GetAchievementBadgePath ( next_cheevo , false ) ;
}
void Achievements : : PrefetchAllAchievementBadges ( )
{
static constexpr u32 PREFETCH_IMAGE_TYPE = RC_IMAGE_TYPE_ACHIEVEMENT ;
// This is here so that we can hopefully avoid the delay in downloading the badge image on unlock.
if ( ! HasAchievements ( ) )
return ;
rc_client_achievement_list_t * const achievements =
rc_client_create_achievement_list ( Achievements : : GetClient ( ) , RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL ,
RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE ) ;
if ( ! achievements )
return ;
for ( u32 i = 0 ; i < achievements - > num_buckets ; i + + )
{
// Ignore unlocked achievements, since we're not going to be showing a notification for them.
const rc_client_achievement_bucket_t & bucket = achievements - > buckets [ i ] ;
if ( bucket . bucket_type ! = RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED )
continue ;
for ( u32 j = 0 ; j < bucket . num_achievements ; j + + )
{
const rc_client_achievement_t * const cheevo = bucket . achievements [ j ] ;
std : : string path = GetLocalImagePath ( cheevo - > badge_name , PREFETCH_IMAGE_TYPE ) ;
if ( path . empty ( ) | | FileSystem : : FileExists ( path . c_str ( ) ) )
continue ;
std : : string url = GetAchievementBadgeURL ( cheevo , PREFETCH_IMAGE_TYPE ) ;
VERBOSE_LOG ( " Prefetching badge for locked achievement '{}' ({}) " , cheevo - > title , cheevo - > badge_url ) ;
s_state . prefetch_badge_requests . emplace_back ( std : : move ( path ) , std : : move ( url ) ) ;
}
}
rc_client_destroy_achievement_list ( achievements ) ;
if ( s_state . prefetch_badge_requests . empty ( ) )
return ;
// reverse the list, fetch the first achievement first since it's the most likely to be unlocked next
std : : ranges : : reverse ( s_state . prefetch_badge_requests ) ;
SendNextPrefetchBadgeRequest ( ) ;
}
void Achievements : : SendNextPrefetchBadgeRequest ( )
{
if ( s_state . prefetch_badge_requests . empty ( ) )
return ;
std : : string cache_path = std : : move ( s_state . prefetch_badge_requests . back ( ) . first ) ;
std : : string url = std : : move ( s_state . prefetch_badge_requests . back ( ) . second ) ;
s_state . prefetch_badge_requests . pop_back ( ) ;
// free memory when done
if ( s_state . prefetch_badge_requests . empty ( ) )
s_state . prefetch_badge_requests = { } ;
auto callback = [ cache_path = std : : move ( cache_path ) ] ( s32 status_code , const Error & error ,
const std : : string & content_type ,
HTTPDownloader : : Request : : Data data ) mutable {
if ( status_code ! = HTTPDownloader : : HTTP_STATUS_OK )
{
ERROR_LOG ( " Failed to download badge '{}': {} " , Path : : GetFileName ( cache_path ) , error . GetDescription ( ) ) ;
return ;
}
Error write_error ;
if ( ! FileSystem : : WriteBinaryFile ( cache_path . c_str ( ) , data , & write_error ) )
{
ERROR_LOG ( " Failed to write badge image to '{}': {} " , cache_path , write_error . GetDescription ( ) ) ;
return ;
}
VideoThread : : RunOnThread (
[ cache_path = std : : move ( cache_path ) ] ( ) { FullscreenUI : : InvalidateCachedTexture ( cache_path ) ; } ) ;
SendNextPrefetchBadgeRequest ( ) ;
} ;
s_state . http_downloader - > CreateRequest ( std : : move ( url ) , std : : move ( callback ) ) ;
if ( ! s_state . prefetch_badge_requests . empty ( ) )
VERBOSE_LOG ( " {} badge requests remaining " , s_state . prefetch_badge_requests . size ( ) ) ;
}
void Achievements : : ClearPrefetchBadgeRequests ( )
{
s_state . prefetch_badge_requests = { } ;
}
bool Achievements : : IsActive ( )
{
return ( s_state . client ! = nullptr ) ;
@ -1130,6 +1223,8 @@ void Achievements::GameChanged(CDImage* image)
if ( ! IdentifyGame ( image ) )
return ;
ClearPrefetchBadgeRequests ( ) ;
// cancel previous requests
if ( s_state . load_game_request )
{
@ -1200,6 +1295,9 @@ void Achievements::BeginLoadGame()
return ;
}
// Clear prefetch requests, since if we're loading state we'll get blocked until they all download otherwise.
ClearPrefetchBadgeRequests ( ) ;
s_state . load_game_request = rc_client_begin_load_game ( s_state . client , GameHashToString ( s_state . game_hash ) . c_str ( ) ,
ClientLoadGameCallback , nullptr ) ;
}
@ -1294,7 +1392,12 @@ void Achievements::ClientLoadGameCallback(int result, const char* error_message,
// update progress database on first load, in case it was played on another PC
UpdateGameSummary ( true ) ;
PrefetchNextAchievementBadge ( ) ;
// Defer starting the prefetch, because otherwise when loading state we'll block until it's all downloaded.
if ( g_settings . achievements_prefetch_badges )
Host : : RunOnCoreThread ( & Achievements : : PrefetchAllAchievementBadges ) ;
else
PrefetchNextAchievementBadge ( ) ;
// needed for notifications
SoundEffectManager : : EnsureInitialized ( ) ;
@ -1307,6 +1410,8 @@ void Achievements::ClearGameInfo()
{
FullscreenUI : : ClearAchievementsState ( ) ;
ClearPrefetchBadgeRequests ( ) ;
if ( s_state . load_game_request )
{
rc_client_abort_async ( s_state . client , s_state . load_game_request ) ;
@ -1958,6 +2063,24 @@ bool Achievements::DoState(StateWrapper& sw)
}
}
std : : string Achievements : : GetAchievementBadgeURL ( const rc_client_achievement_t * achievement , u32 image_type )
{
std : : string url ;
const char * url_ptr ;
// RAIntegration doesn't set the URL fields.
if ( IsUsingRAIntegration ( ) | |
! ( url_ptr =
( image_type = = RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED ) ? achievement - > badge_locked_url : achievement - > badge_url ) )
{
return GetImageURL ( achievement - > badge_name , image_type ) ;
}
else
{
return std : : string ( url_ptr ) ;
}
}
std : : string Achievements : : GetAchievementBadgePath ( const rc_client_achievement_t * achievement , bool locked ,
bool download_if_missing )
{
@ -1965,15 +2088,7 @@ std::string Achievements::GetAchievementBadgePath(const rc_client_achievement_t*
const std : : string path = GetLocalImagePath ( achievement - > badge_name , image_type ) ;
if ( download_if_missing & & ! path . empty ( ) & & ! FileSystem : : FileExists ( path . c_str ( ) ) )
{
std : : string url ;
const char * url_ptr ;
// RAIntegration doesn't set the URL fields.
if ( IsUsingRAIntegration ( ) | | ! ( url_ptr = locked ? achievement - > badge_locked_url : achievement - > badge_url ) )
url = GetImageURL ( achievement - > badge_name , image_type ) ;
else
url = std : : string ( url_ptr ) ;
std : : string url = GetAchievementBadgeURL ( achievement , image_type ) ;
if ( url . empty ( ) ) [[unlikely]]
{
ReportFmtError ( " Achievement {} with badge name {} has no badge URL " , achievement - > id , achievement - > badge_name ) ;
@ -1981,7 +2096,7 @@ std::string Achievements::GetAchievementBadgePath(const rc_client_achievement_t*
else
{
DEV_LOG ( " Downloading badge for achievement {} from URL: {} " , achievement - > id , url ) ;
DownloadImage ( std : : string ( url ) , path ) ;
DownloadImage ( std : : move ( url ) , path ) ;
}
}