From dbd1a9117bbcfeef000436930becd7be2a5c0056 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Wed, 29 Jul 2026 21:00:00 -0600 Subject: [PATCH 1/3] update to rcheevos 12.4 --- deps/rcheevos/CHANGELOG.md | 13 ++ deps/rcheevos/include/rc_api_info.h | 18 ++ deps/rcheevos/include/rc_api_user.h | 4 + deps/rcheevos/include/rc_client.h | 57 +++++ deps/rcheevos/include/rc_error.h | 3 +- deps/rcheevos/src/rapi/rc_api_common.c | 191 ++++++++++++--- deps/rcheevos/src/rapi/rc_api_common.h | 6 + deps/rcheevos/src/rapi/rc_api_info.c | 85 ++++--- deps/rcheevos/src/rapi/rc_api_runtime.c | 13 +- deps/rcheevos/src/rapi/rc_api_user.c | 28 ++- deps/rcheevos/src/rc_client.c | 258 ++++++++++++++++++--- deps/rcheevos/src/rc_client_external.c | 1 + deps/rcheevos/src/rc_util.c | 3 +- deps/rcheevos/src/rc_version.h | 4 +- deps/rcheevos/src/rcheevos/alloc.c | 4 + deps/rcheevos/src/rcheevos/condition.c | 7 +- deps/rcheevos/src/rcheevos/consoleinfo.c | 16 +- deps/rcheevos/src/rcheevos/memref.c | 1 + deps/rcheevos/src/rcheevos/richpresence.c | 10 +- deps/rcheevos/src/rcheevos/runtime.c | 4 +- deps/rcheevos/src/rcheevos/value.c | 10 +- deps/rcheevos/src/rhash/cdreader.c | 39 +++- deps/rcheevos/src/rhash/hash.c | 16 +- deps/rcheevos/src/rhash/hash_disc.c | 20 +- deps/rcheevos/src/rhash/hash_rom.c | 96 +++++++- deps/rcheevos/src/rhash/hash_zip.c | 3 +- deps/rcheevos/src/rhash/rc_hash_internal.h | 1 + menu/menu_displaylist.c | 1 - 28 files changed, 759 insertions(+), 153 deletions(-) diff --git a/deps/rcheevos/CHANGELOG.md b/deps/rcheevos/CHANGELOG.md index fe237f03294d..cb7111580bec 100644 --- a/deps/rcheevos/CHANGELOG.md +++ b/deps/rcheevos/CHANGELOG.md @@ -1,3 +1,16 @@ +# v12.4.0 +* add avatar_last_updated field to rc_client_user_t and rc_api_login_response_t +* add more fields to fetch_games_list API +* add rc_client_begin_fetch_game_list +* add extended RAM to memory map for PSP +* add memory map for XBOX +* add hash generation for .neo files (geolith-specific NeoGeo ROMs) +* add validation warning if Mem and Delta conditions exist for same address and value +* ignore warning achievements when reporting achievement count in rc_client_get_user_game_summary +* fix crash when {recall} exists without a Remember +* fix crash when legacy value contains invalid syntax that overflows the conversion buffer +* fix handling of SubSource chain starting with a constant + # 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 diff --git a/deps/rcheevos/include/rc_api_info.h b/deps/rcheevos/include/rc_api_info.h index 0051fe23e5b8..1894cce1231b 100644 --- a/deps/rcheevos/include/rc_api_info.h +++ b/deps/rcheevos/include/rc_api_info.h @@ -164,8 +164,26 @@ rc_api_fetch_games_list_request_t; typedef struct rc_api_game_list_entry_t { /* The unique identifier of the game */ uint32_t id; + /* The number of achievements in the game */ + uint32_t num_achievements; + /* The number of leaderboards in the game */ + uint32_t num_leaderboards; + /* The number of points in the game */ + uint32_t points; /* The name of the game */ const char* name; + /* The image name for the game badge */ + const char* image_name; + /* The URL for the game badge image */ + const char* image_url; + /* An array of supported hashes */ + const char** supported_hashes; + /* An array of unsupported hashes */ + const char** unsupported_hashes; + /* The number of items in the supported_hashes array */ + uint32_t num_supported_hashes; + /* The number of items in the unsupported_hashes array */ + uint32_t num_unsupported_hashes; } rc_api_game_list_entry_t; diff --git a/deps/rcheevos/include/rc_api_user.h b/deps/rcheevos/include/rc_api_user.h index c977da95640d..f604188aa855 100644 --- a/deps/rcheevos/include/rc_api_user.h +++ b/deps/rcheevos/include/rc_api_user.h @@ -42,6 +42,8 @@ typedef struct rc_api_login_response_t { const char* display_name; /* A URL to the user's avatar image */ const char* avatar_url; + /* When the avatar was last updated */ + time_t avatar_last_updated; /* Common server-provided response information */ rc_api_response_t response; @@ -187,6 +189,8 @@ typedef struct rc_api_followed_user_t { const char* display_name; /* A URL to the user's avatar image */ const char* avatar_url; + /* When the avatar was last updated */ + time_t avatar_last_updated; /* The player's last registered activity */ rc_api_followed_user_activity_t recent_activity; /* The current score of the player */ diff --git a/deps/rcheevos/include/rc_client.h b/deps/rcheevos/include/rc_client.h index 8dde30ab6199..dd1538c07748 100644 --- a/deps/rcheevos/include/rc_client.h +++ b/deps/rcheevos/include/rc_client.h @@ -190,6 +190,8 @@ typedef struct rc_client_user_t { uint32_t num_unread_messages; /* minimum version: 12.0 */ const char* avatar_url; + /* minimum version: 12.4 */ + time_t avatar_last_updated; } rc_client_user_t; /** @@ -450,6 +452,61 @@ RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_fetch_game_titles( */ RC_EXPORT void RC_CCONV rc_client_destroy_game_title_list(rc_client_game_title_list_t* list); +/*****************************************************************************\ +| Fetch Games List | +\*****************************************************************************/ + +typedef struct rc_client_game_list_entry_t { + /* The unique identifier of the game */ + uint32_t id; + /* The number of achievements in the game */ + uint32_t num_achievements; + /* The number of leaderboards in the game */ + uint32_t num_leaderboards; + /* The number of points in the game */ + uint32_t points; + /* The name of the game */ + const char* name; + /* The image name for the game badge */ + const char* image_name; + /* The URL for the game badge image */ + const char* image_url; + /* An array of supported hashes */ + const char** supported_hashes; + /* An array of unsupported hashes */ + const char** unsupported_hashes; + /* The number of items in the supported_hashes array */ + uint32_t num_supported_hashes; + /* The number of items in the unsupported_hashes array */ + uint32_t num_unsupported_hashes; +} rc_client_game_list_entry_t; + +typedef struct rc_client_game_list_t { + rc_client_game_list_entry_t* entries; + uint32_t num_entries; +} rc_client_game_list_t; + +/** + * Callback that is fired when a games list request completes. list may be null if the query failed. + */ +typedef void(RC_CCONV* rc_client_fetch_game_list_callback_t)(int result, const char* error_message, + rc_client_game_list_t* list, rc_client_t* client, + void* callback_userdata); + +/** + * Starts an asynchronous request for all games for the given console. + * This request returns the game metadata and supported/unsupported hashes for each game on the console, + * described by the rc_client_game_list_entry_t struct. After use, the list should be freed by calling + * the rc_client_destroy_game_list() function. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_fetch_game_list( + rc_client_t* client, uint32_t console_id, rc_client_fetch_game_list_callback_t callback, void* callback_userdata); + +/** + * Destroys a previously-allocated result from the rc_client_begin_fetch_game_list() callback. + */ +RC_EXPORT void RC_CCONV rc_client_destroy_game_list(rc_client_game_list_t* list); + /*****************************************************************************\ | Achievements | \*****************************************************************************/ diff --git a/deps/rcheevos/include/rc_error.h b/deps/rcheevos/include/rc_error.h index 5d253442da05..197bcea7023d 100644 --- a/deps/rcheevos/include/rc_error.h +++ b/deps/rcheevos/include/rc_error.h @@ -49,7 +49,8 @@ enum { RC_INSUFFICIENT_BUFFER = -36, RC_INVALID_VARIABLE_NAME = -37, RC_UNKNOWN_VARIABLE_NAME = -38, - RC_NOT_FOUND = -39 + RC_NOT_FOUND = -39, + RC_INVALID_VALUE = -40 }; RC_EXPORT const char* RC_CCONV rc_error_str(int ret); diff --git a/deps/rcheevos/src/rapi/rc_api_common.c b/deps/rcheevos/src/rapi/rc_api_common.c index 3a261fba46c2..75db8f41f08f 100644 --- a/deps/rcheevos/src/rapi/rc_api_common.c +++ b/deps/rcheevos/src/rapi/rc_api_common.c @@ -360,7 +360,7 @@ int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_serv if (server_response->http_status_code == RC_API_SERVER_RESPONSE_CLIENT_ERROR || server_response->http_status_code == RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR) { /* client provided error message is passed as the response body */ - response->error_message = server_response->body; + response->error_message = rc_buffer_strncpy(&response->buffer, server_response->body, server_response->body_length); response->succeeded = 0; return RC_NO_RESPONSE; } @@ -467,30 +467,76 @@ static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_iterato return 1; } +static int rc_json_get_unum_array(uint32_t** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* array, const char* field_name) { + if (*num_entries) { + rc_json_iterator_t iterator; + rc_json_field_t value; + uint32_t* entry; + + *entries = (uint32_t*)rc_buffer_alloc(&response->buffer, *num_entries * sizeof(uint32_t)); + if (!*entries) + return RC_OUT_OF_MEMORY; + + value.name = field_name; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array->value_start; + iterator.end = array->value_end; + + entry = *entries; + while (rc_json_get_array_entry_value(&value, &iterator)) { + if (!rc_json_get_unum(entry, &value, field_name)) + return RC_MISSING_VALUE; + + ++entry; + } + } + else { + *entries = NULL; + } + + return RC_OK; +} + int rc_json_get_required_unum_array(uint32_t** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { - rc_json_iterator_t iterator; rc_json_field_t array; - rc_json_field_t value; - uint32_t* entry; - memset(&array, 0, sizeof(array)); + if (!rc_json_get_required_array(num_entries, &array, response, field, field_name)) return RC_MISSING_VALUE; + return rc_json_get_unum_array(entries, num_entries, response, &array, field_name); +} + +int rc_json_get_optional_unum_array(uint32_t** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + rc_json_field_t array; + memset(&array, 0, sizeof(array)); + + if (!rc_json_get_optional_array(num_entries, &array, field, field_name)) + *num_entries = 0; + + return rc_json_get_unum_array(entries, num_entries, response, &array, field_name); +} + +static int rc_json_get_string_array(const char*** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* array, const char* field_name) { if (*num_entries) { - *entries = (uint32_t*)rc_buffer_alloc(&response->buffer, *num_entries * sizeof(uint32_t)); + rc_json_iterator_t iterator; + rc_json_field_t value; + const char** entry; + + *entries = (const char**)rc_buffer_alloc(&response->buffer, *num_entries * sizeof(const char*)); if (!*entries) return RC_OUT_OF_MEMORY; value.name = field_name; memset(&iterator, 0, sizeof(iterator)); - iterator.json = array.value_start; - iterator.end = array.value_end; + iterator.json = array->value_start; + iterator.end = array->value_end; entry = *entries; while (rc_json_get_array_entry_value(&value, &iterator)) { - if (!rc_json_get_unum(entry, &value, field_name)) + if (!rc_json_get_string(entry, &response->buffer, &value, field_name)) return RC_MISSING_VALUE; ++entry; @@ -503,6 +549,26 @@ int rc_json_get_required_unum_array(uint32_t** entries, uint32_t* num_entries, r return RC_OK; } +int rc_json_get_required_string_array(const char*** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + rc_json_field_t array; + + memset(&array, 0, sizeof(array)); + if (!rc_json_get_required_array(num_entries, &array, response, field, field_name)) + return RC_MISSING_VALUE; + + return rc_json_get_string_array(entries, num_entries, response, &array, field_name); +} + +int rc_json_get_optional_string_array(const char*** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + rc_json_field_t array; + + memset(&array, 0, sizeof(array)); + if (!rc_json_get_optional_array(num_entries, &array, field, field_name)) + *num_entries = 0; + + return rc_json_get_string_array(entries, num_entries, response, &array, field_name); +} + int rc_json_get_required_array(uint32_t* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) @@ -623,6 +689,12 @@ int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_fiel return 0; } + if (len == 0) { + /* simple optimization for empty string - don't allocate space */ + *out = ""; + return 1; + } + if (len == 4 && memcmp(field->value_start, "null", 4) == 0) { *out = NULL; return 1; @@ -658,7 +730,11 @@ int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_fiel if (*src == 'u') { /* unicode character */ - uint32_t ucs32_char = rc_json_decode_hex4(src + 1); + uint32_t ucs32_char; + if (src + 5 >= field->value_end) /* incomplete unicode character */ + return 0; + + ucs32_char = rc_json_decode_hex4(src + 1); src += 5; if (ucs32_char >= 0xD800 && ucs32_char < 0xE000) { @@ -710,7 +786,7 @@ int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_fiel int rc_json_field_string_matches(const rc_json_field_t* field, const char* text) { int is_quoted = 0; const char* ptr = field->value_start; - if (!ptr) + if (!ptr || !text) return 0; if (*ptr == '"') { @@ -924,7 +1000,7 @@ int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* (void)field_name; #endif - if (*field->value_start == '\"') { + if (field->value_start && *field->value_start == '\"') { memset(&tm, 0, sizeof(tm)); if (sscanf_s(field->value_start + 1, "%d-%d-%d %d:%d:%d", /* DB format "2013-10-20 22:12:21" */ &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6 || @@ -963,6 +1039,61 @@ int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, cons return rc_json_missing_field(response, field); } +int rc_json_get_timet(time_t* out, const rc_json_field_t* field, const char* field_name) +{ + const char* src = field->value_start; + int64_t value = 0; + int negative = 0; + +#ifndef NDEBUG + if (strcmp(field->name, field_name) != 0) + return 0; +#else + (void)field_name; +#endif + + if (!src) { + *out = 0; + return 0; + } + + /* assert: string contains only numerals and an optional sign per rc_json_parse_field */ + if (*src == '-') { + negative = 1; + ++src; + } else if (*src == '+') { + ++src; + } else if (*src < '0' || *src > '9') { + *out = 0; + return 0; + } + + while (src < field->value_end && *src != '.') { + value *= 10; + value += *src - '0'; + ++src; + } + + if (negative) + *out = (time_t)-value; + else + *out = (time_t)value; + + return 1; +} + +void rc_json_get_optional_timet(time_t* out, const rc_json_field_t* field, const char* field_name, time_t default_value) { + if (!rc_json_get_timet(out, field, field_name)) + *out = default_value; +} + +int rc_json_get_required_timet(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { + if (rc_json_get_timet(out, field, field_name)) + return 1; + + return rc_json_missing_field(response, field); +} + int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name) { const char* src = field->value_start; @@ -1004,23 +1135,25 @@ int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_js } void rc_json_extract_filename(rc_json_field_t* field) { - if (field->value_end) { + if (field->value_end && field->value_end > field->value_start) { const char* str = field->value_end; - - /* remove the extension */ - while (str > field->value_start && str[-1] != '/') { - --str; - if (*str == '.') { - field->value_end = str; - break; + if (str[-1] == '"') { + /* ignore trailing quote */ + field->value_end = --str; + + while (str > field->value_start) { + const char c = *(--str); + if (c == '.') { + /* found an extension. remove it */ + field->value_end = str; + } + else if (c == '/' || c == '"') { + /* found path separator or opening quote. stop */ + field->value_start = str + 1; + break; + } } } - - /* find the path separator */ - while (str > field->value_start && str[-1] != '/') - --str; - - field->value_start = str; } } @@ -1296,12 +1429,15 @@ int rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetc int rc_api_init_fetch_image_request_hosted(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params, const rc_api_host_t* host) { rc_api_url_builder_t builder; + if (!api_params->image_name || !api_params->image_name[0]) + return RC_INVALID_STATE; + rc_buffer_init(&request->buffer); rc_url_builder_init(&builder, &request->buffer, 64); if (host && host->media_host) { /* custom media host provided */ - if (!strstr(host->host, "://")) + if (!strstr(host->media_host, "://")) rc_url_builder_append(&builder, "http://", 7); rc_url_builder_append(&builder, host->media_host, strlen(host->media_host)); } @@ -1358,6 +1494,7 @@ int rc_api_init_fetch_image_request_hosted(rc_api_request_t* request, const rc_a request->url = rc_url_builder_finalize(&builder); request->post_data = NULL; + request->content_type = NULL; return builder.result; } diff --git a/deps/rcheevos/src/rapi/rc_api_common.h b/deps/rcheevos/src/rapi/rc_api_common.h index 78e10d49b932..06a7485da35a 100644 --- a/deps/rcheevos/src/rapi/rc_api_common.h +++ b/deps/rcheevos/src/rapi/rc_api_common.h @@ -49,21 +49,27 @@ int rc_json_get_num(int32_t* out, const rc_json_field_t* field, const char* fiel int rc_json_get_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name); int rc_json_get_float(float* out, const rc_json_field_t* field, const char* field_name); int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_name); +int rc_json_get_timet(time_t* out, const rc_json_field_t* field, const char* field_name); int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* field_name); void rc_json_get_optional_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name, const char* default_value); void rc_json_get_optional_num(int32_t* out, const rc_json_field_t* field, const char* field_name, int default_value); void rc_json_get_optional_unum(uint32_t* out, const rc_json_field_t* field, const char* field_name, uint32_t default_value); void rc_json_get_optional_float(float* out, const rc_json_field_t* field, const char* field_name, float default_value); void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value); +void rc_json_get_optional_timet(time_t* out, const rc_json_field_t* field, const char* field_name, time_t default_value); int rc_json_get_optional_array(uint32_t* num_entries, rc_json_field_t* iterator, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_num(int32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_unum(uint32_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_float(float* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_timet(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name); int rc_json_get_required_unum_array(uint32_t** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_optional_unum_array(uint32_t** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_required_string_array(const char*** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_optional_string_array(const char*** entries, uint32_t* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_array(uint32_t* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator); int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field); diff --git a/deps/rcheevos/src/rapi/rc_api_info.c b/deps/rcheevos/src/rapi/rc_api_info.c index a1fa7e5d5323..327a6dff3fc8 100644 --- a/deps/rcheevos/src/rapi/rc_api_info.c +++ b/deps/rcheevos/src/rapi/rc_api_info.c @@ -56,7 +56,6 @@ int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achieveme rc_api_achievement_awarded_entry_t* entry; rc_json_field_t array_field; rc_json_iterator_t iterator; - uint32_t timet; int result; rc_json_field_t fields[] = { @@ -120,9 +119,8 @@ int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achieveme if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) return RC_MISSING_VALUE; - if (!rc_json_get_required_unum(&timet, &response->response, &entry_fields[1], "DateAwarded")) + if (!rc_json_get_required_timet(&entry->awarded, &response->response, &entry_fields[1], "DateAwarded")) return RC_MISSING_VALUE; - entry->awarded = (time_t)timet; rc_json_get_optional_string(&entry->avatar_url, &response->response, &entry_fields[2], "AvatarUrl", NULL); if (!entry->avatar_url) @@ -185,7 +183,6 @@ int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboa rc_api_lboard_info_entry_t* entry; rc_json_field_t array_field; rc_json_iterator_t iterator; - uint32_t timet; int result; size_t len; char format[16]; @@ -289,9 +286,8 @@ int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboa if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[3], "Score")) return RC_MISSING_VALUE; - if (!rc_json_get_required_unum(&timet, &response->response, &entry_fields[4], "DateSubmitted")) + if (!rc_json_get_required_timet(&entry->submitted, &response->response, &entry_fields[4], "DateSubmitted")) return RC_MISSING_VALUE; - entry->submitted = (time_t)timet; rc_json_get_optional_string(&entry->avatar_url, &response->response, &entry_fields[5], "AvatarUrl", NULL); if (!entry->avatar_url) @@ -325,8 +321,8 @@ int rc_api_init_fetch_games_list_request_hosted(rc_api_request_t* request, return RC_INVALID_STATE; rc_url_builder_init(&builder, &request->buffer, 48); - rc_url_builder_append_str_param(&builder, "r", "gameslist"); - rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); + rc_url_builder_append_str_param(&builder, "r", "systemgames"); + rc_url_builder_append_unum_param(&builder, "s", api_params->console_id); request->post_data = rc_url_builder_finalize(&builder); request->content_type = RC_CONTENT_TYPE_URLENCODED; @@ -347,9 +343,8 @@ int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response) { rc_api_game_list_entry_t* entry; rc_json_iterator_t iterator; - rc_json_field_t field; + rc_json_field_t array_field; int result; - char* end; rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), @@ -357,6 +352,18 @@ int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_resp RC_JSON_NEW_FIELD("Response") }; + rc_json_field_t game_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("ImageIcon"), + RC_JSON_NEW_FIELD("ImageUrl"), + RC_JSON_NEW_FIELD("NumAchievements"), + RC_JSON_NEW_FIELD("NumLeaderboards"), + RC_JSON_NEW_FIELD("Points"), + RC_JSON_NEW_FIELD("SupportedHashes"), /* array */ + RC_JSON_NEW_FIELD("UnsupportedHashes"), /* array */ + }; + memset(response, 0, sizeof(*response)); rc_buffer_init(&response->response.buffer); @@ -364,32 +371,50 @@ int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_resp if (result != RC_OK) return result; - if (!fields[2].value_start) { - /* call rc_json_get_required_object to generate the error message */ - rc_json_get_required_object(NULL, 0, &response->response, &fields[2], "Response"); + if (!rc_json_get_required_array(&response->num_entries, &array_field, &response->response, &fields[2], "Response")) return RC_MISSING_VALUE; - } - response->num_entries = fields[2].array_size; - rc_buffer_reserve(&response->response.buffer, response->num_entries * (32 + sizeof(rc_api_game_list_entry_t))); + if (response->num_entries) { + /* 8=image_name, 32=title, 64=image_url, 32=one hash */ + rc_buffer_reserve(&response->response.buffer, response->num_entries * (8 + 32 + 64 + 32 + sizeof(rc_api_game_list_entry_t))); - response->entries = (rc_api_game_list_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_game_list_entry_t)); - if (!response->entries) - return RC_OUT_OF_MEMORY; + response->entries = (rc_api_game_list_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_game_list_entry_t)); + if (!response->entries) + return RC_OUT_OF_MEMORY; - memset(&iterator, 0, sizeof(iterator)); - iterator.json = fields[2].value_start; - iterator.end = fields[2].value_end; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; - entry = response->entries; - while (rc_json_get_next_object_field(&iterator, &field)) { - entry->id = (uint32_t)strtol(field.name, &end, 10); + entry = response->entries; + while (rc_json_get_array_entry_object(game_fields, sizeof(game_fields) / sizeof(game_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&entry->id, &response->response, &game_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&entry->name, &response->response, &game_fields[1], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&entry->num_achievements, &response->response, &game_fields[4], "NumAchievements")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&entry->num_leaderboards, &response->response, &game_fields[5], "NumLeaderboards")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&entry->points, &response->response, &game_fields[6], "Points")) + return RC_MISSING_VALUE; - field.name = ""; - if (!rc_json_get_string(&entry->name, &response->response.buffer, &field, "")) - return RC_MISSING_VALUE; + /* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */ + rc_json_extract_filename(&game_fields[2]); + if (!rc_json_get_required_string(&entry->image_name, &response->response, &game_fields[2], "ImageIcon")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&entry->image_url, &response->response, &game_fields[3], "ImageUrl")) + return RC_MISSING_VALUE; - ++entry; + result = rc_json_get_required_string_array(&entry->supported_hashes, &entry->num_supported_hashes, &response->response, &game_fields[7], "SupportedHashes"); + if (result != RC_OK) + return result; + result = rc_json_get_optional_string_array(&entry->unsupported_hashes, &entry->num_unsupported_hashes, &response->response, &game_fields[8], "UnsupportedHashes"); + if (result != RC_OK) + return result; + + ++entry; + } } return RC_OK; @@ -484,7 +509,7 @@ int rc_api_process_fetch_game_titles_server_response(rc_api_fetch_game_titles_re return RC_MISSING_VALUE; rc_json_get_optional_string(&entry->image_url, &response->response, &entry_fields[3], "ImageUrl", ""); - if (!entry->image_url[0]) + if (!entry->image_url || !entry->image_url[0]) entry->image_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_GAME, entry->image_name); ++entry; diff --git a/deps/rcheevos/src/rapi/rc_api_runtime.c b/deps/rcheevos/src/rapi/rc_api_runtime.c index 99a001db2ff5..afecabbac961 100644 --- a/deps/rcheevos/src/rapi/rc_api_runtime.c +++ b/deps/rcheevos/src/rapi/rc_api_runtime.c @@ -113,7 +113,6 @@ static int rc_api_process_fetch_game_data_achievements(rc_api_response_t* respon const char* last_author = ""; const char* last_author_field = ""; size_t last_author_len = 0; - uint32_t timet; size_t len; rc_json_field_t achievement_fields[] = { @@ -155,11 +154,11 @@ static int rc_api_process_fetch_game_data_achievements(rc_api_response_t* respon return RC_MISSING_VALUE; rc_json_get_optional_string(&achievement->badge_url, response, &achievement_fields[13], "BadgeURL", ""); - if (!achievement->badge_url[0]) + if (!achievement->badge_url || !achievement->badge_url[0]) achievement->badge_url = rc_api_build_avatar_url(&response->buffer, RC_IMAGE_TYPE_ACHIEVEMENT, achievement->badge_name); rc_json_get_optional_string(&achievement->badge_locked_url, response, &achievement_fields[14], "BadgeLockedURL", ""); - if (!achievement->badge_locked_url[0]) + if (!achievement->badge_locked_url || !achievement->badge_locked_url[0]) achievement->badge_locked_url = rc_api_build_avatar_url(&response->buffer, RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, achievement->badge_name); len = achievement_fields[6].value_end - achievement_fields[6].value_start; @@ -181,12 +180,10 @@ static int rc_api_process_fetch_game_data_achievements(rc_api_response_t* respon } } - if (!rc_json_get_required_unum(&timet, response, &achievement_fields[8], "Created")) + if (!rc_json_get_required_timet(&achievement->created, response, &achievement_fields[8], "Created")) return RC_MISSING_VALUE; - achievement->created = (time_t)timet; - if (!rc_json_get_required_unum(&timet, response, &achievement_fields[9], "Modified")) + if (!rc_json_get_required_timet(&achievement->updated, response, &achievement_fields[9], "Modified")) return RC_MISSING_VALUE; - achievement->updated = (time_t)timet; if (rc_json_field_string_matches(&achievement_fields[10], "")) achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD; @@ -320,7 +317,7 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon rc_json_extract_filename(&patchdata_fields[3]); rc_json_get_optional_string(&response->image_name, &response->response, &patchdata_fields[3], "ImageIcon", ""); rc_json_get_optional_string(&response->image_url, &response->response, &patchdata_fields[4], "ImageIconURL", ""); - if (!response->image_url[0]) + if (!response->image_url || !response->image_url[0]) response->image_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_GAME, response->image_name); /* estimate the amount of space necessary to store the rich presence script, achievements, and leaderboards. diff --git a/deps/rcheevos/src/rapi/rc_api_user.c b/deps/rcheevos/src/rapi/rc_api_user.c index 6b61611773f0..9970e56c18d4 100644 --- a/deps/rcheevos/src/rapi/rc_api_user.c +++ b/deps/rcheevos/src/rapi/rc_api_user.c @@ -62,7 +62,8 @@ int rc_api_process_login_server_response(rc_api_login_response_t* response, cons RC_JSON_NEW_FIELD("Score"), RC_JSON_NEW_FIELD("SoftcoreScore"), RC_JSON_NEW_FIELD("Messages"), - RC_JSON_NEW_FIELD("AvatarUrl") + RC_JSON_NEW_FIELD("AvatarUrl"), + RC_JSON_NEW_FIELD("AvatarUpdatedAt") }; memset(response, 0, sizeof(*response)); @@ -90,6 +91,8 @@ int rc_api_process_login_server_response(rc_api_login_response_t* response, cons if (!response->avatar_url) response->avatar_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_USER, response->username); + rc_json_get_optional_timet(&response->avatar_last_updated, &fields[9], "AvatarUpdatedAt", 0); + return RC_OK; } @@ -145,7 +148,6 @@ int rc_api_process_start_session_server_response(rc_api_start_session_response_t rc_api_unlock_entry_t* unlock; rc_json_field_t array_field; rc_json_iterator_t iterator; - uint32_t timet; int result; rc_json_field_t fields[] = { @@ -181,9 +183,8 @@ int rc_api_process_start_session_server_response(rc_api_start_session_response_t while (rc_json_get_array_entry_object(unlock_entry_fields, sizeof(unlock_entry_fields) / sizeof(unlock_entry_fields[0]), &iterator)) { if (!rc_json_get_required_unum(&unlock->achievement_id, &response->response, &unlock_entry_fields[0], "ID")) return RC_MISSING_VALUE; - if (!rc_json_get_required_unum(&timet, &response->response, &unlock_entry_fields[1], "When")) + if (!rc_json_get_required_timet(&unlock->when, &response->response, &unlock_entry_fields[1], "When")) return RC_MISSING_VALUE; - unlock->when = (time_t)timet; ++unlock; } @@ -202,16 +203,14 @@ int rc_api_process_start_session_server_response(rc_api_start_session_response_t while (rc_json_get_array_entry_object(unlock_entry_fields, sizeof(unlock_entry_fields) / sizeof(unlock_entry_fields[0]), &iterator)) { if (!rc_json_get_required_unum(&unlock->achievement_id, &response->response, &unlock_entry_fields[0], "ID")) return RC_MISSING_VALUE; - if (!rc_json_get_required_unum(&timet, &response->response, &unlock_entry_fields[1], "When")) + if (!rc_json_get_required_timet(&unlock->when, &response->response, &unlock_entry_fields[1], "When")) return RC_MISSING_VALUE; - unlock->when = (time_t)timet; ++unlock; } } - rc_json_get_optional_unum(&timet, &fields[4], "ServerNow", 0); - response->server_now = (time_t)timet; + rc_json_get_optional_timet(&response->server_now, &fields[4], "ServerNow", 0); return RC_OK; } @@ -307,11 +306,11 @@ int rc_api_process_fetch_followed_users_server_response(rc_api_fetch_followed_us rc_json_field_t array_field; rc_json_iterator_t iterator; rc_api_followed_user_t* user; - uint32_t timet; int result; rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Code"), RC_JSON_NEW_FIELD("Friends") }; @@ -324,6 +323,7 @@ int rc_api_process_fetch_followed_users_server_response(rc_api_fetch_followed_us RC_JSON_NEW_FIELD("LastGameId"), RC_JSON_NEW_FIELD("LastGameTitle"), RC_JSON_NEW_FIELD("LastGameIconUrl"), + RC_JSON_NEW_FIELD("AvatarUpdatedAt"), }; memset(response, 0, sizeof(*response)); @@ -333,7 +333,7 @@ int rc_api_process_fetch_followed_users_server_response(rc_api_fetch_followed_us if (result != RC_OK || !response->response.succeeded) return result; - if (!rc_json_get_required_array(&response->num_users, &array_field, &response->response, &fields[2], "Friends")) + if (!rc_json_get_required_array(&response->num_users, &array_field, &response->response, &fields[3], "Friends")) return RC_MISSING_VALUE; if (response->num_users) { @@ -354,11 +354,9 @@ int rc_api_process_fetch_followed_users_server_response(rc_api_fetch_followed_us if (!rc_json_get_required_unum(&user->score, &response->response, &followed_user_entry_fields[2], "RAPoints")) return RC_MISSING_VALUE; + rc_json_get_optional_timet(&user->avatar_last_updated, &followed_user_entry_fields[8], "AvatarUpdatedAt", 0); rc_json_get_optional_string(&user->recent_activity.description, &response->response, &followed_user_entry_fields[3], "LastSeen", NULL); - - rc_json_get_optional_unum(&timet, &followed_user_entry_fields[4], "LastSeenTime", 0); - user->recent_activity.when = (time_t)timet; - + rc_json_get_optional_timet(&user->recent_activity.when, &followed_user_entry_fields[4], "LastSeenTime", 0); rc_json_get_optional_unum(&user->recent_activity.context_id, &followed_user_entry_fields[5], "LastGameId", 0); rc_json_get_optional_string(&user->recent_activity.context, &response->response, &followed_user_entry_fields[6], "LastGameTitle", NULL); rc_json_get_optional_string(&user->recent_activity.context_image_url, &response->response, &followed_user_entry_fields[7], "LastGameIconUrl", NULL); @@ -457,7 +455,7 @@ int rc_api_process_fetch_all_user_progress_server_response(rc_api_fetch_all_user entry = response->entries; while (rc_json_get_next_object_field(&iterator, &field)) { - entry->game_id = (uint32_t)strtol(field.name, &end, 10); + entry->game_id = strtol(field.name, &end, 10); field.name = ""; if (!rc_json_get_required_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), diff --git a/deps/rcheevos/src/rc_client.c b/deps/rcheevos/src/rc_client.c index 39e4fa293705..24de1bf14b77 100644 --- a/deps/rcheevos/src/rc_client.c +++ b/deps/rcheevos/src/rc_client.c @@ -13,35 +13,16 @@ #include -#if defined(_WIN32) - #if defined(_MSC_VER) && _MSC_VER < 1700 - #include /* pre-VS2012 SDKs lack profileapi.h; windows.h brings the functions */ - #else - #include - #endif -#endif - -/* clock_gettime() was added to Darwin in iOS 10.0 / macOS 10.12. On - * earlier deployment targets the symbol doesn't exist in libSystem - * and rc_client_clock_get_now_millisecs() below fails to link. - * Disable the CLOCK_MONOTONIC branch there so the function falls - * through to the clock()+time() #else fallback. MIN_REQUIRED is - * the deployment target - what the binary must be able to run on - - * which is the right version to gate on here. */ -#if defined(__APPLE__) && defined(__MACH__) -#include -#if (defined(MAC_OS_X_VERSION_MIN_REQUIRED) \ - && MAC_OS_X_VERSION_MIN_REQUIRED < 101200) \ - || (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) \ - && __IPHONE_OS_VERSION_MIN_REQUIRED < 100000) -#ifdef CLOCK_MONOTONIC -#undef CLOCK_MONOTONIC -#endif -#endif +#ifdef _WIN32 + #define WIN32_LEAN_AND_MEAN + #include +#else + #include #endif #define RC_CLIENT_UNKNOWN_GAME_ID (uint32_t)-1 #define RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS (10 * 60) /* ten minutes */ +#define RC_CLIENT_ACHIEVEMENT_WARNING_ID 101000001 #define RC_MINIMUM_UNPAUSED_FRAMES 20 #define RC_PAUSE_DECAY_MULTIPLIER 4 @@ -259,7 +240,7 @@ static void rc_client_log_message_va(const rc_client_t* client, const char* form char buffer[2048]; #ifdef __STDC_SECURE_LIB__ - vsprintf_s(buffer, sizeof(buffer), format, args); + vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, format, args); #elif __STDC_VERSION__ >= 199901L /* vsnprintf requires c99 */ vsnprintf(buffer, sizeof(buffer), format, args); #else /* c89 doesn't have a size-limited vsprintf function - assume the buffer is large enough */ @@ -339,6 +320,22 @@ void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_ static rc_clock_t rc_client_clock_get_now_millisecs(const rc_client_t* client) { +#if defined(__APPLE__) && defined(__MACH__) + #ifdef CLOCK_MONOTONIC + /* clock_gettime() was added to Darwin in iOS 10.0 and macOS 10.12. + * On earlier deployment targets (like Leopard 10.5), the symbol doesn't exist + * in libSystem causing an "undefined reference to clock_gettime" linker error. + * To get the code to use the #else block below, forcibly undefine CLOCK_MONOTONIC + * when targeting earlier versions. */ + #include + #if (defined(MAC_OS_X_VERSION_MIN_REQUIRED) && MAC_OS_X_VERSION_MIN_REQUIRED < 101200) + #undef CLOCK_MONOTONIC + #elif (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < 100000) + #undef CLOCK_MONOTONIC + #endif + #endif +#endif + #if defined(CLOCK_MONOTONIC) struct timespec now; (void)client; @@ -722,6 +719,7 @@ static void rc_client_login_callback(const rc_api_server_response_t* server_resp client->user.display_name = rc_buffer_strcpy(&client->state.buffer, login_response.display_name); client->user.avatar_url = rc_buffer_strcpy(&client->state.buffer, login_response.avatar_url); + client->user.avatar_last_updated = login_response.avatar_last_updated; client->user.token = rc_buffer_strcpy(&client->state.buffer, login_response.api_token); client->user.score = login_response.score; client->user.score_softcore = login_response.score_softcore; @@ -953,6 +951,11 @@ static void rc_client_subset_get_user_game_summary(const rc_client_t* client, for (; achievement < stop; ++achievement) { switch (achievement->public_.category) { case RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE: + if (achievement->public_.id >= RC_CLIENT_ACHIEVEMENT_WARNING_ID) { + /* ignore warning achievements */ + continue; + } + ++summary->num_core_achievements; summary->points_core += achievement->public_.points; @@ -2128,10 +2131,10 @@ static void rc_client_copy_achievements(rc_client_load_state_t* load_state, else { rc_buffer_consume(buffer, (const uint8_t*)preparse.parse.buffer, (uint8_t*)preparse.parse.buffer + preparse.parse.offset); } - - rc_destroy_preparse_state(&preparse); } + rc_destroy_preparse_state(&preparse); + achievement->created_time = read->created; achievement->updated_time = read->updated; @@ -3900,6 +3903,193 @@ void rc_client_destroy_game_title_list(rc_client_game_title_list_t* list) free(list); } +/* ===== Fetch Games List ===== */ + +typedef struct rc_client_fetch_game_list_callback_data_t { + rc_client_t* client; + rc_client_fetch_game_list_callback_t callback; + void* callback_userdata; + rc_client_async_handle_t async_handle; +} rc_client_fetch_game_list_callback_data_t; + +static void rc_client_fetch_game_list_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_fetch_game_list_callback_data_t* list_callback_data = + (rc_client_fetch_game_list_callback_data_t*)callback_data; + rc_client_t* client = list_callback_data->client; + rc_api_fetch_games_list_response_t list_response; + const char* error_message; + int result; + + result = rc_client_end_async(client, &list_callback_data->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) + RC_CLIENT_LOG_VERBOSE(client, "Fetch game list aborted"); + + free(list_callback_data); + return; + } + + result = rc_api_process_fetch_games_list_server_response(&list_response, server_response); + error_message = + rc_client_server_error_message(&result, server_response->http_status_code, &list_response.response); + if (error_message) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch game list failed: %s", error_message); + list_callback_data->callback(result, error_message, NULL, client, list_callback_data->callback_userdata); + } else { + rc_client_game_list_t* list; + size_t strings_size = 0, hashes_size = 0; + const rc_api_game_list_entry_t* src; + const rc_api_game_list_entry_t* stop; + size_t list_size; + uint32_t i; + + /* calculate string buffer size */ + for (src = list_response.entries, stop = src + list_response.num_entries; src < stop; ++src) { + if (src->name) + strings_size += strlen(src->name) + 1; + if (src->image_name) + strings_size += strlen(src->image_name) + 1; + if (src->image_url) + strings_size += strlen(src->image_url) + 1; + hashes_size += src->num_supported_hashes * sizeof(const char*); + for (i = 0; i < src->num_supported_hashes; i++) + strings_size += strlen(src->supported_hashes[i]) + 1; + hashes_size += src->num_unsupported_hashes * sizeof(const char*); + for (i = 0; i < src->num_unsupported_hashes; i++) + strings_size += strlen(src->unsupported_hashes[i]) + 1; + } + + list_size = sizeof(*list) + sizeof(rc_client_game_list_entry_t) * list_response.num_entries + + sizeof(const char*) * hashes_size + strings_size; + list = (rc_client_game_list_t*)malloc(list_size); + if (!list) { + list_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, + list_callback_data->callback_userdata); + } else { + rc_client_game_list_entry_t* entry = list->entries = + (rc_client_game_list_entry_t*)((uint8_t*)list + sizeof(*list)); + const char** hash_list = + (const char**)((uint8_t*)entry + sizeof(rc_client_game_list_entry_t) * list_response.num_entries); + char* strings = (char*)((uint8_t*)hash_list + sizeof(const char*) * hashes_size); + size_t len; + + for (src = list_response.entries, stop = src + list_response.num_entries; src < stop; ++src, ++entry) { + entry->id = src->id; + entry->num_achievements = src->num_achievements; + entry->num_leaderboards = src->num_leaderboards; + entry->points = src->points; + + if (src->name) { + len = strlen(src->name) + 1; + entry->name = strings; + memcpy(strings, src->name, len); + strings += len; + } else { + entry->name = NULL; + } + + if (src->image_name) { + len = strlen(src->image_name) + 1; + entry->image_name = strings; + memcpy(strings, src->image_name, len); + strings += len; + } else { + entry->image_name = NULL; + } + + if (src->image_url) { + len = strlen(src->image_url) + 1; + entry->image_url = strings; + memcpy(strings, src->image_url, len); + strings += len; + } else { + entry->image_url = NULL; + } + + if ((entry->num_supported_hashes = src->num_supported_hashes) > 0) { + entry->supported_hashes = hash_list; + for (i = 0; i < src->num_supported_hashes; i++) { + len = strlen(src->supported_hashes[i]) + 1; + *(hash_list++) = strings; + memcpy(strings, src->supported_hashes[i], len); + strings += len; + } + } else { + entry->supported_hashes = NULL; + } + + if ((entry->num_unsupported_hashes = src->num_unsupported_hashes) > 0) { + entry->unsupported_hashes = hash_list; + for (i = 0; i < src->num_unsupported_hashes; i++) { + len = strlen(src->unsupported_hashes[i]) + 1; + *(hash_list++) = strings; + memcpy(strings, src->unsupported_hashes[i], len); + strings += len; + } + } else { + entry->unsupported_hashes = NULL; + } + } + + list->num_entries = list_response.num_entries; + + list_callback_data->callback(RC_OK, NULL, list, client, list_callback_data->callback_userdata); + } + } + + rc_api_destroy_fetch_games_list_response(&list_response); + free(list_callback_data); +} + +rc_client_async_handle_t* rc_client_begin_fetch_game_list(rc_client_t* client, uint32_t console_id, + rc_client_fetch_game_list_callback_t callback, + void* callback_userdata) +{ + rc_api_fetch_games_list_request_t api_params; + rc_client_fetch_game_list_callback_data_t* callback_data; + rc_client_async_handle_t* async_handle; + rc_api_request_t request; + int result; + const char* error_message; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", NULL, client, callback_userdata); + return NULL; + } + + api_params.console_id = console_id; + result = rc_api_init_fetch_games_list_request_hosted(&request, &api_params, &client->state.host); + + if (result != RC_OK) { + error_message = rc_error_str(result); + callback(result, error_message, NULL, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_fetch_game_list_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + + async_handle = &callback_data->async_handle; + rc_client_begin_async(client, async_handle); + client->callbacks.server_call(&request, rc_client_fetch_game_list_callback, callback_data, client); + rc_api_destroy_request(&request); + + return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; +} + +void rc_client_destroy_game_list(rc_client_game_list_t* list) +{ + free(list); +} + /* ===== Achievements ===== */ static void rc_client_update_achievement_display_information(rc_client_t* client, rc_client_achievement_info_t* achievement, time_t recent_unlock_time) @@ -3947,7 +4137,7 @@ static void rc_client_update_achievement_display_information(rc_client_t* client if (!achievement->trigger->measured_as_percent) { char* ptr = achievement->public_.measured_progress; const int buffer_size = (int)sizeof(achievement->public_.measured_progress); - const int chars = rc_format_value(ptr, buffer_size, (int32_t)new_measured_value, RC_FORMAT_UNSIGNED_VALUE); + const int chars = rc_format_value(ptr, buffer_size - 1, (int32_t)new_measured_value, RC_FORMAT_UNSIGNED_VALUE); ptr[chars] = '/'; rc_format_value(ptr + chars + 1, buffer_size - chars - 1, (int32_t)achievement->trigger->measured_target, RC_FORMAT_UNSIGNED_VALUE); } @@ -4625,6 +4815,11 @@ static void rc_client_award_achievement(rc_client_t* client, rc_client_achieveme rc_mutex_unlock(&client->state.mutex); + if (achievement->public_.id >= RC_CLIENT_ACHIEVEMENT_WARNING_ID) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unlocked warning achievement %u: %s", achievement->public_.id, achievement->public_.title); + return; + } + if (client->callbacks.can_submit_achievement_unlock && !client->callbacks.can_submit_achievement_unlock(achievement->public_.id, client)) { RC_CLIENT_LOG_INFO_FORMATTED(client, "Achievement %u unlock blocked by client", achievement->public_.id); @@ -4652,7 +4847,6 @@ static void rc_client_award_achievement(rc_client_t* client, rc_client_achieveme callback_data->client = client; callback_data->id = achievement->public_.id; callback_data->hardcore = client->state.hardcore; - callback_data->game_hash = client->game->public_.hash; callback_data->unlock_time = client->callbacks.get_time_millisecs(client); if (client->game) /* may be NULL if this gets called while unloading the game (from another thread - events are raised outside the lock) */ @@ -5829,9 +6023,6 @@ static void rc_client_update_memref_values(rc_client_t* client) { } while (modified_memref_list); } - if (client->game->runtime.richpresence && client->game->runtime.richpresence->richpresence) - rc_update_values(client->game->runtime.richpresence->richpresence->values, client->state.legacy_peek, client); - if (invalidated_memref) rc_client_update_active_achievements(client->game); } @@ -5861,6 +6052,7 @@ static void rc_client_do_frame_process_achievements(rc_client_t* client, rc_clie /* if the measured value changed and the achievement hasn't triggered, show a progress indicator */ if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN && trigger->measured_value <= trigger->measured_target && + trigger->measured_target != 0 && rc_trigger_state_active(new_state) && new_state != RC_TRIGGER_STATE_WAITING) { /* only show a popup for the achievement closest to triggering */ diff --git a/deps/rcheevos/src/rc_client_external.c b/deps/rcheevos/src/rc_client_external.c index c65371ddf29a..88abcc9f01de 100644 --- a/deps/rcheevos/src/rc_client_external.c +++ b/deps/rcheevos/src/rc_client_external.c @@ -43,6 +43,7 @@ static const char* rc_client_external_build_avatar_url(char buffer[], size_t buf return NULL; snprintf(buffer, buffer_size, "%s", request.url); + rc_api_destroy_request(&request); return buffer; } diff --git a/deps/rcheevos/src/rc_util.c b/deps/rcheevos/src/rc_util.c index cd750d2c3c9c..95de413dcb15 100644 --- a/deps/rcheevos/src/rc_util.c +++ b/deps/rcheevos/src/rc_util.c @@ -121,7 +121,7 @@ char* rc_buffer_strncpy(rc_buffer_t* buffer, const char* src, size_t len) uint8_t* dst = rc_buffer_reserve(buffer, len + 1); memcpy(dst, src, len); dst[len] = '\0'; - rc_buffer_consume(buffer, dst, dst + len + 2); + rc_buffer_consume(buffer, dst, dst + len + 1); return (char*)dst; } @@ -194,6 +194,7 @@ const char* rc_error_str(int ret) case RC_INVALID_VARIABLE_NAME: return "Invalid variable name"; case RC_UNKNOWN_VARIABLE_NAME: return "Unknown variable name"; case RC_NOT_FOUND: return "Not found"; + case RC_INVALID_VALUE: return "Invalid value expression"; default: return "Unknown error"; } } diff --git a/deps/rcheevos/src/rc_version.h b/deps/rcheevos/src/rc_version.h index 30acbced61a0..7eaa32a862c2 100644 --- a/deps/rcheevos/src/rc_version.h +++ b/deps/rcheevos/src/rc_version.h @@ -8,8 +8,8 @@ RC_BEGIN_C_DECLS #define RCHEEVOS_VERSION_MAJOR 12 -#define RCHEEVOS_VERSION_MINOR 2 -#define RCHEEVOS_VERSION_PATCH 1 +#define RCHEEVOS_VERSION_MINOR 4 +#define RCHEEVOS_VERSION_PATCH 0 #define RCHEEVOS_MAKE_VERSION(major, minor, patch) (major * 1000000 + minor * 1000 + patch) #define RCHEEVOS_VERSION RCHEEVOS_MAKE_VERSION(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR, RCHEEVOS_VERSION_PATCH) diff --git a/deps/rcheevos/src/rcheevos/alloc.c b/deps/rcheevos/src/rcheevos/alloc.c index 5dbf20cb0f0d..96fb8e382511 100644 --- a/deps/rcheevos/src/rcheevos/alloc.c +++ b/deps/rcheevos/src/rcheevos/alloc.c @@ -213,6 +213,10 @@ static void rc_preparse_sync_operand(rc_operand_t* operand, rc_parse_state_t* pa { if (rc_operand_is_memref(operand) || rc_operand_is_recall(operand)) { const rc_memref_t* src_memref = operand->value.memref; + if (!src_memref) { + parse->offset = RC_INVALID_MEMORY_OPERAND; + return; + } if (src_memref->value.memref_type == RC_MEMREF_TYPE_MODIFIED_MEMREF) { const rc_modified_memref_list_t* modified_memref_list = &memrefs->modified_memrefs; diff --git a/deps/rcheevos/src/rcheevos/condition.c b/deps/rcheevos/src/rcheevos/condition.c index fcb7788a817b..63986812e30c 100644 --- a/deps/rcheevos/src/rcheevos/condition.c +++ b/deps/rcheevos/src/rcheevos/condition.c @@ -166,7 +166,7 @@ static int rc_parse_operator(const char** memaddr) { void rc_condition_convert_to_operand(const rc_condition_t* condition, rc_operand_t* operand, rc_parse_state_t* parse) { if (condition->oper == RC_OPERATOR_NONE) { if (operand != &condition->operand1) - *operand = condition->operand1; + memcpy(operand, &condition->operand1, sizeof(*operand)); } else { uint8_t new_size = RC_MEMSIZE_32_BITS; @@ -411,10 +411,13 @@ void rc_condition_update_parse_state(rc_condition_t* condition, rc_parse_state_t negate = rc_alloc_modified_memref(parse, new_size, &parse->addsource_parent, RC_OPERATOR_SUB_PARENT, &zero); parse->addsource_parent.value.memref = (rc_memref_t*)negate; parse->addsource_parent.size = zero.size; + + if (parse->addsource_parent.type == RC_OPERAND_CONST) + parse->addsource_parent.value.num = rc_get_modified_memref_value(negate, NULL, NULL); } /* subtract the condition from the chain */ - parse->addsource_oper = rc_operand_is_memref(&parse->addsource_parent) ? RC_OPERATOR_SUB_ACCUMULATOR : RC_OPERATOR_SUB_PARENT; + parse->addsource_oper = RC_OPERATOR_SUB_ACCUMULATOR; rc_condition_convert_to_operand(condition, &cond_operand, parse); rc_operand_addsource(&cond_operand, parse, new_size); memcpy(&parse->addsource_parent, &cond_operand, sizeof(cond_operand)); diff --git a/deps/rcheevos/src/rcheevos/consoleinfo.c b/deps/rcheevos/src/rcheevos/consoleinfo.c index aeecb38d2c53..a7ad9afff4c9 100644 --- a/deps/rcheevos/src/rcheevos/consoleinfo.c +++ b/deps/rcheevos/src/rcheevos/consoleinfo.c @@ -818,12 +818,14 @@ static const rc_memory_region_t _rc_memory_regions_playstation2[] = { static const rc_memory_regions_t rc_memory_regions_playstation2 = { _rc_memory_regions_playstation2, 3 }; /* ===== PlayStation Portable ===== */ -/* https://github.com/uofw/upspd/wiki/Memory-map */ +/* https://github.com/uofw/upspd/wiki/Memory-map + * the RAM is extended at loadtime if the game has a MEMSIZE flag in its PARAM.SFO */ static const rc_memory_region_t _rc_memory_regions_psp[] = { { 0x00000000U, 0x007FFFFFU, 0x08000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, { 0x00800000U, 0x01FFFFFFU, 0x08800000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x02000000U, 0x03FFFFFFU, 0x0A000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Extended RAM" }, }; -static const rc_memory_regions_t rc_memory_regions_psp = { _rc_memory_regions_psp, 2 }; +static const rc_memory_regions_t rc_memory_regions_psp = { _rc_memory_regions_psp, 3 }; /* ===== Pokemon Mini ===== */ /* https://www.pokemon-mini.net/documentation/memory-map/ */ @@ -993,6 +995,13 @@ static const rc_memory_region_t _rc_memory_regions_wonderswan[] = { }; static const rc_memory_regions_t rc_memory_regions_wonderswan = { _rc_memory_regions_wonderswan, 2 }; +/* ===== Xbox ===== */ +static const rc_memory_region_t _rc_memory_regions_xbox[] = { + /* Retail Xbox memory map: https://xboxdevwiki.net/Memory */ + { 0x00000000U, 0x03FFFFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_xbox = { _rc_memory_regions_xbox, 1 }; + /* ===== ZX Spectrum ===== */ /* https://github.com/TASEmulators/BizHawk/blob/3a3b22c/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum16K/ZX16.cs * https://github.com/TASEmulators/BizHawk/blob/3a3b22c/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Memory.cs @@ -1206,6 +1215,9 @@ const rc_memory_regions_t* rc_console_memory_regions(uint32_t console_id) case RC_CONSOLE_WONDERSWAN: return &rc_memory_regions_wonderswan; + case RC_CONSOLE_XBOX: + return &rc_memory_regions_xbox; + case RC_CONSOLE_ZX_SPECTRUM: return &rc_memory_regions_zx_spectrum; diff --git a/deps/rcheevos/src/rcheevos/memref.c b/deps/rcheevos/src/rcheevos/memref.c index 9c8f16066e7d..577c7e483336 100644 --- a/deps/rcheevos/src/rcheevos/memref.c +++ b/deps/rcheevos/src/rcheevos/memref.c @@ -742,6 +742,7 @@ uint32_t rc_get_modified_memref_value(const rc_modified_memref_t* memref, rc_pee break; case RC_OPERATOR_SUB_ACCUMULATOR: + /* sub accumulator is "parent - modifier" */ rc_typed_value_negate(&modifier); /* fallthrough */ /* to case RC_OPERATOR_SUB_ACCUMULATOR */ diff --git a/deps/rcheevos/src/rcheevos/richpresence.c b/deps/rcheevos/src/rcheevos/richpresence.c index b2f85c69e17c..229266730130 100644 --- a/deps/rcheevos/src/rcheevos/richpresence.c +++ b/deps/rcheevos/src/rcheevos/richpresence.c @@ -38,6 +38,11 @@ static void rc_alloc_helper_variable_memref_value(rc_richpresence_display_part_t /* ensure new needed memrefs are allocated in the primary buffer */ rc_preparse_copy_memrefs(parse, &preparse.memrefs); + if (parse->offset < 0) { + rc_destroy_preparse_state(&preparse); + return; + } + /* parse the value into the scratch buffer so we can look at it */ rc_reset_parse_state(&preparse.parse, rc_buffer_alloc(&preparse.parse.scratch.buffer, (size_t)size)); preparse.parse.memrefs = parse->memrefs; @@ -725,14 +730,15 @@ rc_memrefs_t* rc_richpresence_get_memrefs(rc_richpresence_t* self) { void rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud, void* unused_L) { (void)unused_L; - rc_update_richpresence_memrefs(richpresence, peek, peek_ud); - rc_update_values(richpresence->values, peek, peek_ud); rc_update_richpresence_internal(richpresence, peek, peek_ud); } void rc_update_richpresence_internal(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud) { rc_richpresence_display_t* display; + rc_update_richpresence_memrefs(richpresence, peek, peek_ud); + rc_update_values(richpresence->values, peek, peek_ud); + for (display = richpresence->first_display; display; display = display->next) { if (display->has_required_hits) rc_test_trigger(&display->trigger, peek, peek_ud, NULL); diff --git a/deps/rcheevos/src/rcheevos/runtime.c b/deps/rcheevos/src/rcheevos/runtime.c index 9c05985ee8a7..c1100b9af9a9 100644 --- a/deps/rcheevos/src/rcheevos/runtime.c +++ b/deps/rcheevos/src/rcheevos/runtime.c @@ -510,7 +510,7 @@ int rc_runtime_get_richpresence(const rc_runtime_t* self, char* buffer, size_t b void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, void* unused_L) { rc_runtime_event_t runtime_event; - int i; + int32_t i; runtime_event.value = 0; @@ -670,7 +670,7 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha break; case RC_LBOARD_STATE_TRIGGERED: - if (lboard_state != RC_RUNTIME_EVENT_LBOARD_TRIGGERED) { + if (lboard_state != RC_LBOARD_STATE_TRIGGERED) { runtime_event.type = RC_RUNTIME_EVENT_LBOARD_TRIGGERED; runtime_event.id = self->lboards[i].id; event_handler(&runtime_event); diff --git a/deps/rcheevos/src/rcheevos/value.c b/deps/rcheevos/src/rcheevos/value.c index 33faee5d5e57..8876f5bced01 100644 --- a/deps/rcheevos/src/rcheevos/value.c +++ b/deps/rcheevos/src/rcheevos/value.c @@ -120,6 +120,12 @@ static void rc_parse_legacy_value(rc_value_t* self, const char** memaddr, rc_par /* extract the next clause */ for (;; ++(*memaddr)) { + if (ptr == &buffer[sizeof(buffer)]) { + /* ran out of local buffer space for converting the condition */ + parse->offset = RC_INVALID_VALUE; + return; + } + switch (**memaddr) { case '_': /* add next */ *ptr = '\0'; @@ -176,7 +182,7 @@ static void rc_parse_legacy_value(rc_value_t* self, const char** memaddr, rc_par if (*buffer_ptr) { /* whatever we copied as a single condition was not fully consumed */ - parse->offset = RC_INVALID_COMPARISON; + parse->offset = RC_INVALID_VALUE; return; } @@ -429,7 +435,7 @@ rc_value_t* rc_alloc_variable(const char* memaddr, size_t memaddr_len, rc_parse_ /* no match found, create a new entry */ value = RC_ALLOC_SCRATCH(rc_value_t, parse); - memset(value, 0, sizeof(value->value)); + memset(value, 0, sizeof(*value)); value->value.size = RC_MEMSIZE_VARIABLE; value->next = NULL; diff --git a/deps/rcheevos/src/rhash/cdreader.c b/deps/rcheevos/src/rhash/cdreader.c index 9ffcc55bcbac..cf4647b29598 100644 --- a/deps/rcheevos/src/rhash/cdreader.c +++ b/deps/rcheevos/src/rhash/cdreader.c @@ -648,20 +648,45 @@ static void* cdreader_open_gdi_track(const char* path, uint32_t track, const rc_ while (isspace((unsigned char)*ptr)) ++ptr; - ptr2 = file; if (*ptr == '\"') { - ++ptr; + ptr2 = ++ptr; /* ignore leading quote */ while (*ptr != '\"') - *ptr2++ = *ptr++; - ++ptr; + { + ++ptr; + + if (ptr >= end) + { + if (iterator->callbacks.filereader.close) + iterator->callbacks.filereader.close(file_handle); + + rc_hash_iterator_error(iterator, "Quoted string without closing quote"); + return NULL; + } + } + + num_read = ptr - ptr2; + ++ptr; /* ignore trailing quote */ } else { - while (*ptr != ' ') - *ptr2++ = *ptr++; + ptr2 = ptr; + while (*ptr != ' ' && ptr < end) + ++ptr; + num_read = ptr - ptr2; } - *ptr2 = '\0'; + + if (num_read >= sizeof(file)) + { + if (iterator->callbacks.filereader.close) + iterator->callbacks.filereader.close(file_handle); + + rc_hash_iterator_error_formatted(iterator, "Cannot copy %u byte filename into %u byte buffer", (unsigned)num_read, (unsigned)sizeof(file)); + return NULL; + } + + memcpy(file, ptr2, num_read); + file[num_read] = '\0'; if (track == current_track) { diff --git a/deps/rcheevos/src/rhash/hash.c b/deps/rcheevos/src/rhash/hash.c index fe6f3ca9d0db..451a9a8945b9 100644 --- a/deps/rcheevos/src/rhash/hash.c +++ b/deps/rcheevos/src/rhash/hash.c @@ -494,7 +494,7 @@ static int rc_hash_file_from_buffer(char hash[33], uint32_t console_id, const rc result = rc_hash_from_file(hash, console_id, &buffered_file_iterator); - buffered_file_iterator.path = NULL; + buffered_file_iterator.path = NULL; /* prevent attempt to free static "memory stream" string */ rc_hash_destroy_iterator(&buffered_file_iterator); return result; } @@ -543,6 +543,11 @@ static int rc_hash_from_buffer(char hash[33], uint32_t console_id, const rc_hash return rc_hash_buffer(hash, iterator->buffer, iterator->buffer_size, iterator); #ifndef RC_HASH_NO_ROM + case RC_CONSOLE_ARCADE: + /* .neo (Geolith Neo Geo cart) files carry the ROM data; other arcade + * formats are archives, which aren't hashed from a buffer. */ + return rc_hash_neogeo_cart(hash, iterator); + case RC_CONSOLE_ARDUBOY: return rc_hash_arduboy(hash, iterator); @@ -862,6 +867,11 @@ static int rc_hash_from_file(char hash[33], uint32_t console_id, const rc_hash_i #ifndef RC_HASH_NO_ROM case RC_CONSOLE_ARCADE: + /* .neo files (Geolith Neo Geo cart format) contain the actual ROM data, + * so are content-hashed. Everything else (.zip/.7z) hashes by filename. */ + if (rc_path_compare_extension(path, "neo")) + return rc_hash_neogeo_cart(hash, iterator); + return rc_hash_arcade(hash, iterator); case RC_CONSOLE_ARDUBOY: @@ -971,7 +981,7 @@ void rc_hash_merge_callbacks(rc_hash_iterator_t* iterator, const rc_hash_callbac if (callbacks->verbose_message) iterator->callbacks.verbose_message = callbacks->verbose_message; if (callbacks->error_message) - iterator->callbacks.verbose_message = callbacks->error_message; + iterator->callbacks.error_message = callbacks->error_message; if (callbacks->filereader.open) memcpy(&iterator->callbacks.filereader, &callbacks->filereader, sizeof(callbacks->filereader)); @@ -1230,6 +1240,7 @@ static const rc_hash_iterator_ext_handler_entry_t rc_hash_iterator_ext_handlers[ { "n64", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_64 }, { "ndd", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_64 }, { "nds", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_DS }, /* handles both DS and DSi */ + { "neo", rc_hash_initialize_iterator_single, RC_CONSOLE_ARCADE }, /* Geolith Neo Geo cart format */ { "nes", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO }, { "ngc", rc_hash_initialize_iterator_single, RC_CONSOLE_NEOGEO_POCKET }, { "nib", rc_hash_initialize_iterator_nib, 0 }, @@ -1245,6 +1256,7 @@ static const rc_hash_iterator_ext_handler_entry_t rc_hash_iterator_ext_handlers[ { "sg", rc_hash_initialize_iterator_single, RC_CONSOLE_SG1000 }, { "sgx", rc_hash_initialize_iterator_single, RC_CONSOLE_PC_ENGINE }, { "smc", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_NINTENDO }, + { "sms", rc_hash_initialize_iterator_single, RC_CONSOLE_MASTER_SYSTEM }, { "sv", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPERVISION }, { "swc", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_NINTENDO }, { "tap", rc_hash_initialize_iterator_tap, 0 }, diff --git a/deps/rcheevos/src/rhash/hash_disc.c b/deps/rcheevos/src/rhash/hash_disc.c index 83c93545e3df..eb9611743e6d 100644 --- a/deps/rcheevos/src/rhash/hash_disc.c +++ b/deps/rcheevos/src/rhash/hash_disc.c @@ -227,9 +227,8 @@ int rc_hash_3do(char hash[33], const rc_hash_iterator_t* iterator) /* the Opera filesystem stores the volume information in the first 132 bytes of sector 0 * https://github.com/barbeque/3dodump/blob/master/OperaFS-Format.md */ - rc_cd_read_sector(iterator, track_handle, 0, buffer, 132); - - if (memcmp(buffer, operafs_identifier, sizeof(operafs_identifier)) == 0) { + if (rc_cd_read_sector(iterator, track_handle, 0, buffer, 132) >= 132 && + memcmp(buffer, operafs_identifier, sizeof(operafs_identifier)) == 0) { rc_hash_iterator_verbose_formatted(iterator, "Found 3DO CD, title=%.32s", &buffer[0x28]); /* include the volume header in the hash */ @@ -1126,15 +1125,13 @@ static int rc_hash_wii_disc(md5_state_t* md5, const rc_hash_iterator_t* iterator /* Hash main headers */ buffer = (uint8_t*)malloc(CLUSTER_SIZE); - if (!buffer) { - rc_file_close(iterator, file_handle); + if (!buffer) return rc_hash_iterator_error(iterator, "Could not allocate temporary buffer"); - } - rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte main header for [%c%c%c%c%c%c]", - MAIN_HEADER_SIZE, buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5]); rc_file_seek(iterator, file_handle, 0, SEEK_SET); rc_file_read(iterator, file_handle, buffer, MAIN_HEADER_SIZE); + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte main header for [%c%c%c%c%c%c]", + MAIN_HEADER_SIZE, buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5]); md5_append(md5, buffer, MAIN_HEADER_SIZE); /* Hash region code */ @@ -1154,7 +1151,6 @@ static int rc_hash_wii_disc(md5_state_t* md5, const rc_hash_iterator_t* iterator if (total_partition_count == 0) { free(buffer); - rc_file_close(iterator, file_handle); return rc_hash_iterator_error(iterator, "No partitions found"); } @@ -1259,6 +1255,9 @@ static int rc_hash_wiiware(md5_state_t* md5, const rc_hash_iterator_t* iterator, /* Hash TMD */ buffer = (uint8_t*)malloc(tmd_size); + if (!buffer) + return rc_hash_iterator_error(iterator, "Could not allocate TMD buffer"); + rc_file_seek(iterator, file_handle, tmd_start_addr, SEEK_SET); rc_file_read(iterator, file_handle, buffer, tmd_size); rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte TMD", tmd_size); @@ -1291,6 +1290,9 @@ static int rc_hash_wiiware(md5_state_t* md5, const rc_hash_iterator_t* iterator, /* Hash content */ buffer = (uint8_t*)malloc(buffer_size); + if (!buffer) + return rc_hash_iterator_error(iterator, "Could not allocate content buffer"); + rc_file_seek(iterator, file_handle, content_addr, SEEK_SET); rc_file_read(iterator, file_handle, buffer, buffer_size); md5_append(md5, buffer, buffer_size); diff --git a/deps/rcheevos/src/rhash/hash_rom.c b/deps/rcheevos/src/rhash/hash_rom.c index 996c3080c34f..9ac9490083c5 100644 --- a/deps/rcheevos/src/rhash/hash_rom.c +++ b/deps/rcheevos/src/rhash/hash_rom.c @@ -21,7 +21,7 @@ static int rc_hash_iterator_buffer(char hash[33], const rc_hash_iterator_t* iter int rc_hash_7800(char hash[33], const rc_hash_iterator_t* iterator) { /* if the file contains a header, ignore it */ - if (memcmp(&iterator->buffer[1], "ATARI7800", 9) == 0) { + if (iterator->buffer_size > 128 && memcmp(&iterator->buffer[1], "ATARI7800", 9) == 0) { rc_hash_iterator_verbose(iterator, "Ignoring 7800 header"); return rc_hash_unheadered_iterator_buffer(hash, iterator, 128); } @@ -184,7 +184,7 @@ int rc_hash_arduboy(char hash[33], const rc_hash_iterator_t* iterator) int rc_hash_lynx(char hash[33], const rc_hash_iterator_t* iterator) { /* if the file contains a header, ignore it */ - if (memcmp(&iterator->buffer[0], "LYNX", 5) == 0) { + if (iterator->buffer_size > 64 && memcmp(&iterator->buffer[0], "LYNX", 5) == 0) { rc_hash_iterator_verbose(iterator, "Ignoring LYNX header"); return rc_hash_unheadered_iterator_buffer(hash, iterator, 64); } @@ -195,12 +195,12 @@ int rc_hash_lynx(char hash[33], const rc_hash_iterator_t* iterator) int rc_hash_nes(char hash[33], const rc_hash_iterator_t* iterator) { /* if the file contains a header, ignore it */ - if (memcmp(&iterator->buffer[0], "NES\x1a", 4) == 0) { + if (iterator->buffer_size > 16 && memcmp(&iterator->buffer[0], "NES\x1a", 4) == 0) { rc_hash_iterator_verbose(iterator, "Ignoring NES header"); return rc_hash_unheadered_iterator_buffer(hash, iterator, 16); } - if (memcmp(&iterator->buffer[0], "FDS\x1a", 4) == 0) { + if (iterator->buffer_size > 16 && memcmp(&iterator->buffer[0], "FDS\x1a", 4) == 0) { rc_hash_iterator_verbose(iterator, "Ignoring FDS header"); return rc_hash_unheadered_iterator_buffer(hash, iterator, 16); } @@ -297,6 +297,92 @@ int rc_hash_n64(char hash[33], const rc_hash_iterator_t* iterator) return rc_hash_finalize(iterator, &md5, hash); } +int rc_hash_neogeo_cart(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* The first 4096 bytes of a .neo file are a header: a magic number, the + * sizes of the ROM sections, and metadata text fields (name, manufacturer, + * genre, ...). The text fields can differ between conversion tools, so the + * header cannot participate in the hash. The decrypted, concatenated ROM + * data starts at byte 4096 and is deterministic for a given romset - hash + * only that. + * https://github.com/libretro/geolith-libretro (src/geo_neo.c) + */ + const size_t header_size = 4096; + const size_t chunk_size = 65536; + md5_state_t md5; + uint8_t* buffer; + uint8_t header[4]; + int64_t size; + void* file_handle; + size_t remaining; + int result = 0; + + if (iterator->buffer) { + if (iterator->buffer_size < header_size || + memcmp(iterator->buffer, "NEO\1", 4) != 0) + return rc_hash_iterator_error(iterator, "Not a valid .neo file"); + + rc_hash_iterator_verbose(iterator, "Ignoring NEO header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, header_size); + } + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + if (rc_file_read(iterator, file_handle, header, 4) != 4 || + memcmp(header, "NEO\1", 4) != 0) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Not a valid .neo file"); + } + + rc_file_seek(iterator, file_handle, 0, SEEK_END); + size = rc_file_tell(iterator, file_handle); + if (size <= (int64_t)header_size) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Not a valid .neo file"); + } + size -= (int64_t)header_size; + + if (size > MAX_BUFFER_SIZE) { + rc_hash_iterator_verbose_formatted(iterator, "Hashing first %u bytes (of %u bytes) of %s after 4096 byte header", + MAX_BUFFER_SIZE, (unsigned)size, rc_path_get_filename(iterator->path)); + remaining = MAX_BUFFER_SIZE; + } + else { + rc_hash_iterator_verbose_formatted(iterator, "Hashing %s (%u bytes after 4096 byte header)", + rc_path_get_filename(iterator->path), (unsigned)size); + remaining = (size_t)size; + } + + md5_init(&md5); + + buffer = (uint8_t*)malloc(chunk_size); + if (!buffer) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Could not allocate temporary buffer"); + } + + rc_file_seek(iterator, file_handle, (int64_t)header_size, SEEK_SET); + while (remaining >= chunk_size) { + rc_file_read(iterator, file_handle, buffer, (int)chunk_size); + md5_append(&md5, buffer, (int)chunk_size); + remaining -= chunk_size; + } + + if (remaining > 0) { + rc_file_read(iterator, file_handle, buffer, (int)remaining); + md5_append(&md5, buffer, (int)remaining); + } + + free(buffer); + result = rc_hash_finalize(iterator, &md5, hash); + + rc_file_close(iterator, file_handle); + return result; +} + int rc_hash_nintendo_ds(char hash[33], const rc_hash_iterator_t* iterator) { uint8_t header[512]; @@ -405,7 +491,7 @@ int rc_hash_scv(char hash[33], const rc_hash_iterator_t* iterator) { /* if the file contains a header, ignore it */ /* https://gitlab.com/MaaaX-EmuSCV/libretro-emuscv/-/blob/master/readme.txt#L211 */ - if (memcmp(iterator->buffer, "EmuSCV", 6) == 0) { + if (iterator->buffer_size > 32 && memcmp(iterator->buffer, "EmuSCV", 6) == 0) { rc_hash_iterator_verbose(iterator, "Ignoring SCV header"); return rc_hash_unheadered_iterator_buffer(hash, iterator, 32); } diff --git a/deps/rcheevos/src/rhash/hash_zip.c b/deps/rcheevos/src/rhash/hash_zip.c index 060dc938dd53..03a4dfa3bc62 100644 --- a/deps/rcheevos/src/rhash/hash_zip.c +++ b/deps/rcheevos/src/rhash/hash_zip.c @@ -122,7 +122,6 @@ static int rc_hash_zip_file(md5_state_t* md5, void* file_handle, cdir_start = alloc_buf; cdir_max = cdir_start + cdir_size - cdirhdr_size; - cdir = cdir_start; /* Write our temporary hash data to the same buffer we read the central directory from. * We can do that because the amount of data we keep for each file is guaranteed to be less than the file record. @@ -151,7 +150,7 @@ static int rc_hash_zip_file(md5_state_t* md5, void* file_handle, /* Ignore records describing a directory (we only hash file records) */ name = (cdir + cdirhdr_size); - if (name[filename_len - 1] == '/' || name[filename_len - 1] == '\\' || (external_attr & 0x10)) + if (filename_len == 0 || name[filename_len - 1] == '/' || name[filename_len - 1] == '\\' || (external_attr & 0x10)) continue; /* Handle Zip64 fields */ diff --git a/deps/rcheevos/src/rhash/rc_hash_internal.h b/deps/rcheevos/src/rhash/rc_hash_internal.h index f50a9348d2f6..c1ea6d0c7719 100644 --- a/deps/rcheevos/src/rhash/rc_hash_internal.h +++ b/deps/rcheevos/src/rhash/rc_hash_internal.h @@ -74,6 +74,7 @@ int rc_hash_buffered_file(char hash[33], uint32_t console_id, const rc_hash_iter int rc_hash_lynx(char hash[33], const rc_hash_iterator_t* iterator); int rc_hash_nes(char hash[33], const rc_hash_iterator_t* iterator); int rc_hash_n64(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_neogeo_cart(char hash[33], const rc_hash_iterator_t* iterator); int rc_hash_nintendo_ds(char hash[33], const rc_hash_iterator_t* iterator); int rc_hash_pce(char hash[33], const rc_hash_iterator_t* iterator); int rc_hash_scv(char hash[33], const rc_hash_iterator_t* iterator); diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index f75f24788692..d041e0dedf47 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -5351,7 +5351,6 @@ static unsigned menu_displaylist_parse_content_information( size_t _len = strlcpy(tmp, cheevos_hash_str, sizeof(tmp) - 4); tmp[ _len] = ':'; tmp[++_len] = ' '; - tmp[++_len] = '\n'; tmp[++_len] = '\0'; strlcpy(tmp + _len, rcheevos_get_hash(), sizeof(tmp) - _len); if (menu_entries_append(info_list, tmp, cheevos_hash_str, From 9ce2d1865068b4c1e7d5fe0efd32f3510c7e7e77 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sat, 1 Aug 2026 06:52:34 -0600 Subject: [PATCH 2/3] don't try to download badges from ssl site if HAVE_SSL is not defined --- cheevos/cheevos_client.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/cheevos/cheevos_client.c b/cheevos/cheevos_client.c index 1322b8db0338..c9dadeea243f 100644 --- a/cheevos/cheevos_client.c +++ b/cheevos/cheevos_client.c @@ -439,6 +439,8 @@ typedef struct rc_client_download_task_data_t rc_client_download_queue_t* queue; char badge_fullpath[PATH_MAX_LENGTH]; char badge_name[32]; + char url[256]; + int retry_count; } rc_client_download_task_data_t; static void rcheevos_client_download_task_callback(retro_task_t* task, @@ -454,6 +456,17 @@ static void rcheevos_client_download_task_callback(retro_task_t* task, else if (http_data->status != 200) { CHEEVOS_LOG(RCHEEVOS_TAG "HTTP status code %d for badge %s\n", http_data->status, callback_data->badge_name); + + /* -1 status code is an internal networking failure. go ahead and retry a few times */ + if (http_data->status == -1 && callback_data->retry_count++ < 4) + { + rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + + task_push_http_transfer_with_user_agent(callback_data->url, + true, "GET", rcheevos_locals->user_agent_core, + rcheevos_client_download_task_callback, callback_data); + return; + } } else if (!filestream_write_file(callback_data->badge_fullpath, http_data->data, http_data->len)) { @@ -563,8 +576,15 @@ bool rcheevos_client_download_badge(rc_client_download_queue_t* queue, taskdata->queue = queue; strlcpy(taskdata->badge_fullpath, badge_fullpath, sizeof(taskdata->badge_fullpath)); strlcpy(taskdata->badge_name, badge_name, sizeof(taskdata->badge_name)); + strlcpy(taskdata->url, url, sizeof(taskdata->url)); + taskdata->retry_count = 0; + +#ifndef HAVE_SSL + if (string_starts_with(taskdata->url, "https:")) + strlcpy(&taskdata->url[4], &url[5], sizeof(taskdata->url) - 4); +#endif - task_push_http_transfer_with_user_agent(url, + task_push_http_transfer_with_user_agent(taskdata->url, true, "GET", rcheevos_locals->user_agent_core, rcheevos_client_download_task_callback, taskdata); From a5a61f32502087814dbb7b1f337c92e6720bf25e Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sat, 1 Aug 2026 07:26:53 -0600 Subject: [PATCH 3/3] move #include out of function --- deps/rcheevos/src/rc_client.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deps/rcheevos/src/rc_client.c b/deps/rcheevos/src/rc_client.c index 24de1bf14b77..7a7399580ebe 100644 --- a/deps/rcheevos/src/rc_client.c +++ b/deps/rcheevos/src/rc_client.c @@ -318,8 +318,6 @@ void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_ /* ===== Common ===== */ -static rc_clock_t rc_client_clock_get_now_millisecs(const rc_client_t* client) -{ #if defined(__APPLE__) && defined(__MACH__) #ifdef CLOCK_MONOTONIC /* clock_gettime() was added to Darwin in iOS 10.0 and macOS 10.12. @@ -336,6 +334,8 @@ static rc_clock_t rc_client_clock_get_now_millisecs(const rc_client_t* client) #endif #endif +static rc_clock_t rc_client_clock_get_now_millisecs(const rc_client_t* client) +{ #if defined(CLOCK_MONOTONIC) struct timespec now; (void)client;