From 9534881513993477d68af1aa00299f2050b1fa7d Mon Sep 17 00:00:00 2001 From: Jamiras Date: Tue, 10 Feb 2026 10:53:15 -0700 Subject: [PATCH] update to rcheevos 12.3 --- cheevos/cheevos.c | 15 +++ cheevos/cheevos_client.c | 4 +- deps/rcheevos/CHANGELOG.md | 8 ++ deps/rcheevos/include/rc_client.h | 5 + deps/rcheevos/src/rc_client.c | 124 +++++++++++++++--- deps/rcheevos/src/rc_client_external.c | 4 + deps/rcheevos/src/rc_client_external.h | 6 +- deps/rcheevos/src/rcheevos/condset.c | 7 + deps/rcheevos/src/rcheevos/runtime_progress.c | 2 +- 9 files changed, 154 insertions(+), 21 deletions(-) diff --git a/cheevos/cheevos.c b/cheevos/cheevos.c index 21008545b636..755091e9e6c9 100644 --- a/cheevos/cheevos.c +++ b/cheevos/cheevos.c @@ -335,6 +335,15 @@ static void rcheevos_award_achievement(const rc_client_achievement_t* cheevo) snprintf(subtitle, sizeof(subtitle), "%s (%lu)", cheevo->title, (unsigned long)cheevo->points); gfx_widgets_push_achievement(title, subtitle, cheevo->badge_name); + + /* if all badges haven't been loaded, preload the next one assuming it will be the next needed */ + if (!rcheevos_locals.badges_loaded) + { + const rc_client_achievement_t* next_locked_achievement = + rc_client_get_next_achievement_info(rcheevos_locals.client, cheevo, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + if (next_locked_achievement) + rcheevos_client_download_badge_from_url(next_locked_achievement->badge_url, next_locked_achievement->badge_name); + } } else #endif @@ -1436,12 +1445,18 @@ static void rcheevos_finalize_game_load(rc_client_t* client) #endif if (want_badges) /* prefetch the game badge */ { + const rc_client_achievement_t* first_locked_achievement = NULL; const rc_client_game_t* game = rc_client_get_game_info(client); char badge[32]; badge[0] = 'i'; strlcpy(&badge[1], game->badge_name, sizeof(badge) - 1); rcheevos_client_download_badge_from_url(game->badge_url, badge); + + /* predownload the first badge the player hasn't earned, assuming it will be the next to unlock */ + first_locked_achievement = rc_client_get_next_achievement_info(client, NULL, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + if (first_locked_achievement) + rcheevos_client_download_badge_from_url(first_locked_achievement->badge_url, first_locked_achievement->badge_name); } if (!rc_client_is_processing_required(client)) diff --git a/cheevos/cheevos_client.c b/cheevos/cheevos_client.c index 5e255e83a546..4dd42a1689dd 100644 --- a/cheevos/cheevos_client.c +++ b/cheevos/cheevos_client.c @@ -327,7 +327,7 @@ void rcheevos_client_server_call(const rc_api_request_t* request, rcheevos_log_post_url(request->url, request->post_data); #ifdef CHEEVOS_JSON_OVERRIDE - if (strstr(request->post_data, "r=patch")) + if (strstr(request->post_data, "r=patch") || strstr(request->post_data, "r=achievementsets")) { rcheevos_client_http_load_response(request, callback, callback_data); return; @@ -335,7 +335,7 @@ void rcheevos_client_server_call(const rc_api_request_t* request, #endif #ifdef CHEEVOS_SAVE_JSON - if (strstr(request->post_data, "r=patch")) + if (strstr(request->post_data, "r=patch") || strstr(request->post_data, "r=achievementsets")) { task_push_http_post_transfer_with_user_agent(request->url, request->post_data, true, "POST", rcheevos_locals->user_agent_core, diff --git a/deps/rcheevos/CHANGELOG.md b/deps/rcheevos/CHANGELOG.md index 9b269b3e2ab0..fe237f03294d 100644 --- a/deps/rcheevos/CHANGELOG.md +++ b/deps/rcheevos/CHANGELOG.md @@ -1,3 +1,11 @@ +# v12.3.0 +* add rc_client_get_next_achievement_info +* rc_client image functions will now return RC_INSUFFICENT_BUFFER instead of truncating if buffer is not large enough +* fix race condition where rich presence from previous game may get associated to current game +* fix rc_client_has_leaderboards returning true if the game only has hidden leaderboards +* fix memory leak parsing large achievements +* fix incomplete rich presence display condition affecting later display conditions + # v12.2.1 * fix parsing of leaderboards with comparisons in legacy-formatted values * fix validation warning on long AddSource chains diff --git a/deps/rcheevos/include/rc_client.h b/deps/rcheevos/include/rc_client.h index de69cace621e..8dde30ab6199 100644 --- a/deps/rcheevos/include/rc_client.h +++ b/deps/rcheevos/include/rc_client.h @@ -523,6 +523,11 @@ typedef struct rc_client_achievement_t { */ RC_EXPORT const rc_client_achievement_t* RC_CCONV rc_client_get_achievement_info(rc_client_t* client, uint32_t id); +/** + * Gets the next achievement after a provided achievement that fits in the specified bucket. Returns NULL if none found. + */ +RC_EXPORT const rc_client_achievement_t * RC_CCONV rc_client_get_next_achievement_info(rc_client_t * client, const rc_client_achievement_t * achievement, int bucket); + /** * Gets the URL for the achievement image. * Returns RC_OK on success. diff --git a/deps/rcheevos/src/rc_client.c b/deps/rcheevos/src/rc_client.c index dc104dd23d2e..08ea05340ec4 100644 --- a/deps/rcheevos/src/rc_client.c +++ b/deps/rcheevos/src/rc_client.c @@ -637,7 +637,13 @@ static int rc_client_get_image_url(char buffer[], size_t buffer_size, int image_ image_request.image_name = image_name; result = rc_api_init_fetch_image_request_hosted(&request, &image_request, NULL); if (result == RC_OK) - snprintf(buffer, buffer_size, "%s", request.url); + { + const size_t len = strlen(request.url); + if (len >= buffer_size) + result = RC_INSUFFICIENT_BUFFER; + else + memcpy(buffer, request.url, len + 1); + } rc_api_destroy_request(&request); return result; @@ -898,7 +904,11 @@ int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], si return RC_INVALID_STATE; if (user->avatar_url) { - snprintf(buffer, buffer_size, "%s", user->avatar_url); + const size_t len = strlen(user->avatar_url); + if (len >= buffer_size) + return RC_INSUFFICIENT_BUFFER; + + memcpy(buffer, user->avatar_url, len + 1); return RC_OK; } @@ -3514,7 +3524,11 @@ int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], si return RC_INVALID_STATE; if (game->badge_url) { - snprintf(buffer, buffer_size, "%s", game->badge_url); + const size_t len = strlen(game->badge_url); + if (len >= buffer_size) + return RC_INSUFFICIENT_BUFFER; + + memcpy(buffer, game->badge_url, len + 1); return RC_OK; } @@ -4307,6 +4321,62 @@ const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* clien return NULL; } +const rc_client_achievement_t* rc_client_get_next_achievement_info(rc_client_t* client, + const rc_client_achievement_t* achievement, int bucket) +{ + const rc_client_achievement_info_t* after = (const rc_client_achievement_info_t*)achievement; + rc_client_achievement_info_t* achievement_info; + time_t recent_unlock_time; + rc_client_subset_info_t* subset; + + if (!client) + return NULL; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_next_achievement_info) + return client->state.external_client->get_next_achievement_info(achievement ? achievement->id : 0, bucket); +#endif + + if (!client->game) + return NULL; + + recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active && subset->public_.num_achievements > 0) { + const rc_client_achievement_info_t* start = subset->achievements; + const rc_client_achievement_info_t* stop = start + subset->public_.num_achievements; + if (after == NULL || (after >= start && after <= stop)) { + /* found a subset containing the provided achievement. look for the next + * achievement matching the requested bucket */ + uint32_t index = after ? (uint32_t)(after - start) + 1 : 0; + do { + if (index >= subset->public_.num_achievements) { + /* done with this subset. find the next active subset with achievements */ + do { + subset = subset->next; + if (!subset) + return NULL; + } while (!subset->active || subset->public_.num_achievements == 0); + + index = 0; + } + + /* found an achievement. check to see if it matches the requested bucket. */ + achievement_info = &subset->achievements[index]; + rc_client_update_achievement_display_information(client, achievement_info, recent_unlock_time); + if (achievement_info->public_.bucket == bucket) + return &achievement_info->public_; + + ++index; + } while (1); + } + } + } + + return NULL; +} + + int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size) { const int image_type = (state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) ? @@ -4316,12 +4386,20 @@ int rc_client_achievement_get_image_url(const rc_client_achievement_t* achieveme return rc_client_get_image_url(buffer, buffer_size, image_type, "00000"); if (image_type == RC_IMAGE_TYPE_ACHIEVEMENT && achievement->badge_url) { - snprintf(buffer, buffer_size, "%s", achievement->badge_url); + const size_t len = strlen(achievement->badge_url); + if (len >= buffer_size) + return RC_INSUFFICIENT_BUFFER; + + memcpy(buffer, achievement->badge_url, len + 1); return RC_OK; } if (image_type == RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED && achievement->badge_locked_url) { - snprintf(buffer, buffer_size, "%s", achievement->badge_locked_url); + const size_t len = strlen(achievement->badge_locked_url); + if (len >= buffer_size) + return RC_INSUFFICIENT_BUFFER; + + memcpy(buffer, achievement->badge_locked_url, len + 1); return RC_OK; } @@ -4870,6 +4948,7 @@ int rc_client_has_leaderboards(rc_client_t* client) { rc_client_subset_info_t* subset; int result; + uint32_t i; if (!client) return 0; @@ -4884,17 +4963,21 @@ int rc_client_has_leaderboards(rc_client_t* client) rc_mutex_lock(&client->state.mutex); - subset = client->game->subsets; result = 0; - for (; subset; subset = subset->next) + for (subset = client->game->subsets; subset; subset = subset->next) { if (!subset->active) continue; - if (subset->public_.num_leaderboards > 0) { - result = 1; - break; + for (i = 0; i < subset->public_.num_leaderboards; ++i) { + if (!subset->leaderboards[i].hidden) { + result = 1; + break; + } } + + if (result) + break; } rc_mutex_unlock(&client->state.mutex); @@ -5438,6 +5521,14 @@ static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, r if (client->state.frames_processed != client->state.frames_at_last_ping) { client->state.frames_at_last_ping = client->state.frames_processed; + memset(&api_params, 0, sizeof(api_params)); + api_params.username = client->user.username; + api_params.api_token = client->user.token; + api_params.game_id = client->game->public_.id; + api_params.rich_presence = buffer; + api_params.game_hash = client->game->public_.hash; + api_params.hardcore = client->state.hardcore; + if (!client->callbacks.rich_presence_override || !client->callbacks.rich_presence_override(client, buffer, sizeof(buffer))) { rc_mutex_lock(&client->state.mutex); @@ -5448,13 +5539,12 @@ static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, r rc_mutex_unlock(&client->state.mutex); } - memset(&api_params, 0, sizeof(api_params)); - api_params.username = client->user.username; - api_params.api_token = client->user.token; - api_params.game_id = client->game->public_.id; - api_params.rich_presence = buffer; - api_params.game_hash = client->game->public_.hash; - api_params.hardcore = client->state.hardcore; + /* there's a miniscule chance the game will be changed out while we're waiting for the lock. + * if that happens, discard this ping. the new game will have scheduled its own ping. + * don't reschedule this one. */ + if (!client->game || client->game->public_.id != api_params.game_id) { + return; + } result = rc_api_init_ping_request_hosted(&request, &api_params, &client->state.host); if (result != RC_OK) { diff --git a/deps/rcheevos/src/rc_client_external.c b/deps/rcheevos/src/rc_client_external.c index 0cf90dbb026a..c65371ddf29a 100644 --- a/deps/rcheevos/src/rc_client_external.c +++ b/deps/rcheevos/src/rc_client_external.c @@ -5,6 +5,8 @@ #include "rc_api_runtime.h" +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + #define RC_CONVERSION_FILL(obj, obj_type, src_type) memset((uint8_t*)obj + sizeof(src_type), 0, sizeof(obj_type) - sizeof(src_type)) /* https://media.retroachievements.org/Badge/123456_lock.png is 58 with null terminator */ @@ -275,3 +277,5 @@ rc_client_achievement_list_t* rc_client_external_convert_v1_achievement_list(con return (rc_client_achievement_list_t*)new_list; } + +#endif /* RC_CLIENT_SUPPORTS_EXTERNAL */ diff --git a/deps/rcheevos/src/rc_client_external.h b/deps/rcheevos/src/rc_client_external.h index d73e581b8db3..b3c0e5eba9bf 100644 --- a/deps/rcheevos/src/rc_client_external.h +++ b/deps/rcheevos/src/rc_client_external.h @@ -46,6 +46,7 @@ typedef void (RC_CCONV* rc_client_external_add_game_hash_func_t)(const char* has struct rc_client_achievement_list_info_t; typedef struct rc_client_achievement_list_info_t* (RC_CCONV *rc_client_external_create_achievement_list_func_t)(int category, int grouping); typedef const rc_client_achievement_t* (RC_CCONV *rc_client_external_get_achievement_info_func_t)(uint32_t id); +typedef const rc_client_achievement_t* (RC_CCONV* rc_client_external_get_next_achievement_info_func_t)(uint32_t id, int grouping); /* NOTE: rc_client_external_create_leaderboard_list_func_t returns an internal wrapper structure which contains the public list * and a destructor function. */ @@ -152,9 +153,12 @@ typedef struct rc_client_external_t /* VERSION 6 */ rc_client_external_create_subset_list_func_t create_subset_list; + /* VERSION 7 */ + rc_client_external_get_next_achievement_info_func_t get_next_achievement_info; + } rc_client_external_t; -#define RC_CLIENT_EXTERNAL_VERSION 5 +#define RC_CLIENT_EXTERNAL_VERSION 7 void rc_client_add_game_hash(rc_client_t* client, const char* hash, uint32_t game_id); void rc_client_load_unknown_game(rc_client_t* client, const char* hash); diff --git a/deps/rcheevos/src/rcheevos/condset.c b/deps/rcheevos/src/rcheevos/condset.c index df067a24f54a..b19d3fd227b4 100644 --- a/deps/rcheevos/src/rcheevos/condset.c +++ b/deps/rcheevos/src/rcheevos/condset.c @@ -134,10 +134,12 @@ static int rc_find_next_classification(const char* memaddr) { break; default: + rc_destroy_parse_state(&parse); return classification; } } while (*memaddr++ == '_'); + rc_destroy_parse_state(&parse); return RC_CONDITION_CLASSIFICATION_OTHER; } @@ -261,6 +263,11 @@ rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse) { next = &self->conditions; + /* prevent bleedthrough of incomplete conditions from other groups */ + parse->addsource_oper = RC_OPERATOR_NONE; + parse->addsource_parent.type = RC_OPERAND_NONE; + parse->indirect_parent.type = RC_OPERAND_NONE; + /* each condition set has a functionally new recall accumulator */ parse->remember.type = RC_OPERAND_NONE; diff --git a/deps/rcheevos/src/rcheevos/runtime_progress.c b/deps/rcheevos/src/rcheevos/runtime_progress.c index 51f7e4b6f925..c87721eb4fc0 100644 --- a/deps/rcheevos/src/rcheevos/runtime_progress.c +++ b/deps/rcheevos/src/rcheevos/runtime_progress.c @@ -913,7 +913,7 @@ uint32_t rc_runtime_progress_size(const rc_runtime_t* runtime, void* unused_L) result = rc_runtime_progress_serialize_internal(&progress); if (result != RC_OK) - return result; + return 0; return progress.offset; }