diff --git a/dep/rcheevos/include/rc_api_runtime.h b/dep/rcheevos/include/rc_api_runtime.h index af064f397..8a0de01b7 100644 --- a/dep/rcheevos/include/rc_api_runtime.h +++ b/dep/rcheevos/include/rc_api_runtime.h @@ -135,29 +135,6 @@ typedef struct rc_api_achievement_definition_t { } rc_api_achievement_definition_t; -/* A game subset definition */ -typedef struct rc_api_subset_definition_t { - /* The unique identifier of the subset */ - uint32_t id; - /* The title of the subset */ - const char* title; - /* The image name for the subset badge */ - const char* image_name; - /* The URL for the subset badge */ - const char* image_url; - - /* An array of achievements for the game */ - rc_api_achievement_definition_t* achievements; - /* The number of items in the achievements array */ - uint32_t num_achievements; - - /* An array of leaderboards for the game */ - rc_api_leaderboard_definition_t* leaderboards; - /* The number of items in the leaderboards array */ - uint32_t num_leaderboards; -} -rc_api_subset_definition_t; - #define RC_ACHIEVEMENT_CATEGORY_CORE 3 #define RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL 5 @@ -193,11 +170,6 @@ typedef struct rc_api_fetch_game_data_response_t { /* The number of items in the leaderboards array */ uint32_t num_leaderboards; - /* An array of subsets for the game */ - rc_api_subset_definition_t* subsets; - /* The number of items in the subsets array */ - uint32_t num_subsets; - /* Common server-provided response information */ rc_api_response_t response; } @@ -210,6 +182,90 @@ RC_EXPORT int RC_CCONV rc_api_process_fetch_game_data_response(rc_api_fetch_game RC_EXPORT int RC_CCONV rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response); RC_EXPORT void RC_CCONV rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response); +/* --- Fetch Game Sets --- */ + +/** + * API parameters for a fetch game data request. + */ +typedef struct rc_api_fetch_game_sets_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + uint32_t game_id; + /* The generated hash of the game to be identified (ignored if game_id is not 0) */ + const char* game_hash; +} +rc_api_fetch_game_sets_request_t; + +#define RC_ACHIEVEMENT_SET_TYPE_CORE 0 +#define RC_ACHIEVEMENT_SET_TYPE_BONUS 1 +#define RC_ACHIEVEMENT_SET_TYPE_SPECIALTY 2 +#define RC_ACHIEVEMENT_SET_TYPE_EXCLUSIVE 3 + +/* A game subset definition */ +typedef struct rc_api_achievement_set_definition_t { + /* The unique identifier of the achievement set */ + uint32_t id; + /* The legacy game_id of the achievement set (used for editor API calls) */ + uint32_t game_id; + /* The title of the achievement set */ + const char* title; + /* The image name for the achievement set badge */ + const char* image_name; + /* The URL for the achievement set badge */ + const char* image_url; + + /* An array of achievements for the achievement set */ + rc_api_achievement_definition_t* achievements; + /* The number of items in the achievements array */ + uint32_t num_achievements; + + /* An array of leaderboards for the achievement set */ + rc_api_leaderboard_definition_t* leaderboards; + /* The number of items in the leaderboards array */ + uint32_t num_leaderboards; + + /* The type of the achievement set */ + uint8_t type; +} +rc_api_achievement_set_definition_t; + +/** + * Response data for a fetch game sets request. + */ +typedef struct rc_api_fetch_game_sets_response_t { + /* The unique identifier of the game */ + uint32_t id; + /* The console associated to the game */ + uint32_t console_id; + /* The title of the game */ + const char* title; + /* The image name for the game badge */ + const char* image_name; + /* The URL for the game badge */ + const char* image_url; + /* The rich presence script for the game to be passed to rc_runtime_activate_richpresence */ + const char* rich_presence_script; + /* The unique identifier of the game to use for session requests (startsession/ping/etc) */ + uint32_t session_game_id; + + /* An array of sets for the game */ + rc_api_achievement_set_definition_t* sets; + /* The number of items in the sets array */ + uint32_t num_sets; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_game_sets_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_game_sets_request(rc_api_request_t* request, const rc_api_fetch_game_sets_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_game_sets_request_hosted(rc_api_request_t* request, const rc_api_fetch_game_sets_request_t* api_params, const rc_api_host_t* host); +RC_EXPORT int RC_CCONV rc_api_process_fetch_game_sets_server_response(rc_api_fetch_game_sets_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_game_sets_response(rc_api_fetch_game_sets_response_t* response); + /* --- Ping --- */ /** diff --git a/dep/rcheevos/include/rc_client.h b/dep/rcheevos/include/rc_client.h index f96570e59..2a19108bb 100644 --- a/dep/rcheevos/include/rc_client.h +++ b/dep/rcheevos/include/rc_client.h @@ -217,8 +217,9 @@ typedef struct rc_client_user_game_summary_t { uint32_t points_core; uint32_t points_unlocked; - time_t beaten_time; /* 0 if not beaten, otherwise the time the game was beaten */ - time_t completed_time; /* 0 if not mastered, otherwise the time the game was mastered */ + /* minimum version: 12.1 */ + time_t beaten_time; + time_t completed_time; } rc_client_user_game_summary_t; /** @@ -272,6 +273,12 @@ RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_identify_and_load_g uint32_t console_id, const char* file_path, const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); + +struct rc_hash_callbacks; +/** + * Provide callback functions for interacting with the file system and processing disc-based files when generating hashes. + */ +RC_EXPORT void rc_client_set_hash_callbacks(rc_client_t* client, const struct rc_hash_callbacks* callbacks); #endif /** @@ -286,9 +293,9 @@ RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_load_game(rc_client RC_EXPORT int RC_CCONV rc_client_get_load_game_state(const rc_client_t* client); enum { RC_CLIENT_LOAD_GAME_STATE_NONE, - RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME, RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN, - RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA, + RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME, + RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA, /* [deprecated] - game data is now returned by identify call */ RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION, RC_CLIENT_LOAD_GAME_STATE_DONE, RC_CLIENT_LOAD_GAME_STATE_ABORTED @@ -330,15 +337,17 @@ RC_EXPORT int RC_CCONV rc_client_game_get_image_url(const rc_client_game_t* game /** * Changes the active disc in a multi-disc game. */ -RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_change_media(rc_client_t* client, const char* file_path, +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_identify_and_change_media(rc_client_t* client, const char* file_path, const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); #endif /** * Changes the active disc in a multi-disc game. */ -RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_change_media_from_hash(rc_client_t* client, const char* hash, +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_change_media(rc_client_t* client, const char* hash, rc_client_callback_t callback, void* callback_userdata); +/* this function was renamed in rcheevos 12.0 */ +#define rc_client_begin_change_media_from_hash rc_client_begin_change_media /*****************************************************************************\ | Subsets | @@ -358,6 +367,8 @@ typedef struct rc_client_subset_t { RC_EXPORT const rc_client_subset_t* RC_CCONV rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id); +RC_EXPORT void RC_CCONV rc_client_get_user_subset_summary(const rc_client_t* client, uint32_t subset_id, rc_client_user_game_summary_t* summary); + /*****************************************************************************\ | Fetch Game Hashes | \*****************************************************************************/ @@ -587,7 +598,7 @@ enum { RC_EXPORT rc_client_leaderboard_list_t* RC_CCONV rc_client_create_leaderboard_list(rc_client_t* client, int grouping); /** - * Destroys a list allocated by rc_client_get_leaderboard_list. + * Destroys a list allocated by rc_client_create_leaderboard_list. */ RC_EXPORT void RC_CCONV rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list); @@ -753,6 +764,11 @@ RC_EXPORT void RC_CCONV rc_client_set_event_handler(rc_client_t* client, rc_clie */ RC_EXPORT void RC_CCONV rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler); +/** + * Specifies whether rc_client is allowed to read memory outside of rc_client_do_frame/rc_client_idle. + */ +RC_EXPORT void RC_CCONV rc_client_set_allow_background_memory_reads(rc_client_t* client, int allowed); + /** * Determines if there are any active achievements/leaderboards/rich presence that need processing. */ diff --git a/dep/rcheevos/include/rc_export.h b/dep/rcheevos/include/rc_export.h index da111056d..9471acd48 100644 --- a/dep/rcheevos/include/rc_export.h +++ b/dep/rcheevos/include/rc_export.h @@ -37,7 +37,7 @@ * #endif */ -#ifdef __cplusplus +#if defined(__cplusplus) && !defined(CXX_BUILD) #define RC_BEGIN_C_DECLS extern "C" { #define RC_END_C_DECLS } #else diff --git a/dep/rcheevos/include/rc_hash.h b/dep/rcheevos/include/rc_hash.h index 8bcb49727..be1d37812 100644 --- a/dep/rcheevos/include/rc_hash.h +++ b/dep/rcheevos/include/rc_hash.h @@ -9,17 +9,19 @@ RC_BEGIN_C_DECLS + struct rc_hash_iterator; + /* ===================================================== */ - /* specifies a function to call when an error occurs to display the error message */ - typedef void (RC_CCONV *rc_hash_message_callback)(const char*); + typedef void (RC_CCONV *rc_hash_message_callback_deprecated)(const char*); + /* specifies a function to call when an error occurs to display the error message */ /* [deprecated] set callbacks in rc_hash_iterator_t */ - RC_EXPORT void RC_CCONV rc_hash_init_error_message_callback(rc_hash_message_callback callback); + RC_EXPORT void RC_CCONV rc_hash_init_error_message_callback(rc_hash_message_callback_deprecated callback); /* specifies a function to call for verbose logging */ /* [deprecated] set callbacks in rc_hash_iterator_t */ - RC_EXPORT void rc_hash_init_verbose_message_callback(rc_hash_message_callback callback); + RC_EXPORT void rc_hash_init_verbose_message_callback(rc_hash_message_callback_deprecated callback); /* ===================================================== */ @@ -54,6 +56,8 @@ RC_BEGIN_C_DECLS /* ===================================================== */ +#ifndef RC_HASH_NO_DISC + #define RC_HASH_CDTRACK_FIRST_DATA ((uint32_t)-1) /* the first data track (skip audio tracks) */ #define RC_HASH_CDTRACK_LAST ((uint32_t)-2) /* the last data/audio track */ #define RC_HASH_CDTRACK_LARGEST ((uint32_t)-3) /* the largest data/audio track */ @@ -63,7 +67,7 @@ RC_BEGIN_C_DECLS * returns a handle to be passed to the other functions, or NULL if the track could not be opened. */ typedef void* (RC_CCONV *rc_hash_cdreader_open_track_handler)(const char* path, uint32_t track); - typedef void* (RC_CCONV* rc_hash_cdreader_open_track_filereader_handler)(const char* path, uint32_t track, const rc_hash_filereader_t* filereader); + typedef void* (RC_CCONV* rc_hash_cdreader_open_track_iterator_handler)(const char* path, uint32_t track, const struct rc_hash_iterator* iterator); /* attempts to read the specified number of bytes from the file starting at the specified absolute sector. * returns the number of bytes actually read. @@ -82,7 +86,7 @@ RC_BEGIN_C_DECLS rc_hash_cdreader_read_sector_handler read_sector; rc_hash_cdreader_close_track_handler close_track; rc_hash_cdreader_first_track_sector_handler first_track_sector; - rc_hash_cdreader_open_track_filereader_handler open_track_filereader; + rc_hash_cdreader_open_track_iterator_handler open_track_iterator; } rc_hash_cdreader_t; RC_EXPORT void RC_CCONV rc_hash_get_default_cdreader(struct rc_hash_cdreader* cdreader); @@ -91,6 +95,8 @@ RC_BEGIN_C_DECLS /* [deprecated] set callbacks in rc_hash_iterator_t */ RC_EXPORT void RC_CCONV rc_hash_init_custom_cdreader(struct rc_hash_cdreader* reader); +#endif /* RC_HASH_NO_DISC */ + #ifndef RC_HASH_NO_ENCRYPTED /* specifies a function called to obtain a 3DS CIA decryption normal key. @@ -117,16 +123,20 @@ RC_BEGIN_C_DECLS /* [deprecated] set callbacks in rc_hash_iterator_t */ RC_EXPORT void RC_CCONV rc_hash_init_3ds_get_ncch_normal_keys_func(rc_hash_3ds_get_ncch_normal_keys_func func); -#endif +#endif /* RC_HASH_NO_ENCRYPTED */ /* ===================================================== */ +typedef void (RC_CCONV* rc_hash_message_callback_func)(const char*, const struct rc_hash_iterator* iterator); + typedef struct rc_hash_callbacks { - rc_hash_message_callback verbose_message; - rc_hash_message_callback error_message; + rc_hash_message_callback_func verbose_message; + rc_hash_message_callback_func error_message; rc_hash_filereader_t filereader; +#ifndef RC_HASH_NO_DISC rc_hash_cdreader_t cdreader; +#endif #ifndef RC_HASH_NO_ENCRYPTED struct rc_hash_encryption_callbacks { @@ -144,6 +154,7 @@ typedef struct rc_hash_iterator { uint8_t consoles[12]; int index; const char* path; + void* userdata; rc_hash_callbacks_t callbacks; } rc_hash_iterator_t; diff --git a/dep/rcheevos/src/rapi/rc_api_common.c b/dep/rcheevos/src/rapi/rc_api_common.c index 4caef42a2..3a261fba4 100644 --- a/dep/rcheevos/src/rapi/rc_api_common.c +++ b/dep/rcheevos/src/rapi/rc_api_common.c @@ -273,17 +273,17 @@ static int rc_json_extract_html_error(rc_api_response_t* response, const rc_api_ iterator.json = server_response->body; iterator.end = server_response->body + server_response->body_length; - /* if the title contains an HTTP status code(i.e "404 Not Found"), return the title */ + /* assume the title contains the most appropriate message to display to the user */ if (rc_json_find_substring(&iterator, "")) { const char* title_start = iterator.json + 7; - if (isdigit((int)*title_start) && rc_json_find_substring(&iterator, "")) { + if (rc_json_find_substring(&iterator, "")) { response->error_message = rc_buffer_strncpy(&response->buffer, title_start, iterator.json - title_start); response->succeeded = 0; return RC_INVALID_JSON; } } - /* title not found, or did not start with an error code, return the first line of the response */ + /* title not found, return the first line of the response (up to 200 characters) */ iterator.json = server_response->body; while (iterator.json < iterator.end && *iterator.json != '\n' && @@ -317,6 +317,13 @@ static int rc_json_convert_error_code(const char* server_error_code) case 'i': if (strcmp(server_error_code, "invalid_credentials") == 0) return RC_INVALID_CREDENTIALS; + if (strcmp(server_error_code, "invalid_parameter") == 0) + return RC_INVALID_STATE; + break; + + case 'm': + if (strcmp(server_error_code, "missing_parameter") == 0) + return RC_INVALID_STATE; break; case 'n': @@ -700,6 +707,57 @@ int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_fiel return 1; } +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) + return 0; + + if (*ptr == '"') { + is_quoted = 1; + ++ptr; + } + + while (ptr < field->value_end) { + if (*ptr != *text) { + if (*ptr != '\\') { + if (*ptr == '"' && is_quoted && (*text == '\0')) { + is_quoted = 0; + ++ptr; + continue; + } + + return 0; + } + + ++ptr; + switch (*ptr) { + case 'n': + if (*text != '\n') + return 0; + break; + case 'r': + if (*text != '\r') + return 0; + break; + case 't': + if (*text != '\t') + return 0; + break; + default: + if (*text != *ptr) + return 0; + break; + } + } + + ++text; + ++ptr; + } + + return !is_quoted && (*text == '\0'); +} + 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) { if (!rc_json_get_string(out, &response->buffer, field, field_name)) *out = default_value; diff --git a/dep/rcheevos/src/rapi/rc_api_common.h b/dep/rcheevos/src/rapi/rc_api_common.h index d44260aa7..78e10d49b 100644 --- a/dep/rcheevos/src/rapi/rc_api_common.h +++ b/dep/rcheevos/src/rapi/rc_api_common.h @@ -68,6 +68,7 @@ int rc_json_get_required_array(uint32_t* num_entries, rc_json_field_t* array_fie 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); int rc_json_get_object_string_length(const char* json); +int rc_json_field_string_matches(const rc_json_field_t* field, const char* text); void rc_json_extract_filename(rc_json_field_t* field); diff --git a/dep/rcheevos/src/rapi/rc_api_runtime.c b/dep/rcheevos/src/rapi/rc_api_runtime.c index 49e2a3c40..99a001db2 100644 --- a/dep/rcheevos/src/rapi/rc_api_runtime.c +++ b/dep/rcheevos/src/rapi/rc_api_runtime.c @@ -108,27 +108,13 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r return rc_api_process_fetch_game_data_server_response(response, &response_obj); } -static int rc_parse_achievement_type(const char* type) { - if (strcmp(type, "missable") == 0) - return RC_ACHIEVEMENT_TYPE_MISSABLE; - - if (strcmp(type, "win_condition") == 0) - return RC_ACHIEVEMENT_TYPE_WIN; - - if (strcmp(type, "progression") == 0) - return RC_ACHIEVEMENT_TYPE_PROGRESSION; - - return RC_ACHIEVEMENT_TYPE_STANDARD; -} - -static int rc_api_process_fetch_game_data_achievements(rc_api_fetch_game_data_response_t* response, rc_api_achievement_definition_t* achievement, rc_json_field_t* array_field) { +static int rc_api_process_fetch_game_data_achievements(rc_api_response_t* response, rc_api_achievement_definition_t* achievement, rc_json_field_t* array_field) { rc_json_iterator_t iterator; const char* last_author = ""; const char* last_author_field = ""; size_t last_author_len = 0; uint32_t timet; size_t len; - char type[16]; rc_json_field_t achievement_fields[] = { RC_JSON_NEW_FIELD("ID"), @@ -153,35 +139,35 @@ static int rc_api_process_fetch_game_data_achievements(rc_api_fetch_game_data_re iterator.end = array_field->value_end; while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) { - if (!rc_json_get_required_unum(&achievement->id, &response->response, &achievement_fields[0], "ID")) + if (!rc_json_get_required_unum(&achievement->id, response, &achievement_fields[0], "ID")) return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&achievement->title, &response->response, &achievement_fields[1], "Title")) + if (!rc_json_get_required_string(&achievement->title, response, &achievement_fields[1], "Title")) return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&achievement->description, &response->response, &achievement_fields[2], "Description")) + if (!rc_json_get_required_string(&achievement->description, response, &achievement_fields[2], "Description")) return RC_MISSING_VALUE; - if (!rc_json_get_required_unum(&achievement->category, &response->response, &achievement_fields[3], "Flags")) + if (!rc_json_get_required_unum(&achievement->category, response, &achievement_fields[3], "Flags")) return RC_MISSING_VALUE; - if (!rc_json_get_required_unum(&achievement->points, &response->response, &achievement_fields[4], "Points")) + if (!rc_json_get_required_unum(&achievement->points, response, &achievement_fields[4], "Points")) return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&achievement->definition, &response->response, &achievement_fields[5], "MemAddr")) + if (!rc_json_get_required_string(&achievement->definition, response, &achievement_fields[5], "MemAddr")) return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&achievement->badge_name, &response->response, &achievement_fields[7], "BadgeName")) + if (!rc_json_get_required_string(&achievement->badge_name, response, &achievement_fields[7], "BadgeName")) return RC_MISSING_VALUE; - rc_json_get_optional_string(&achievement->badge_url, &response->response, &achievement_fields[13], "BadgeURL", ""); + rc_json_get_optional_string(&achievement->badge_url, response, &achievement_fields[13], "BadgeURL", ""); if (!achievement->badge_url[0]) - achievement->badge_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_ACHIEVEMENT, achievement->badge_name); + 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->response, &achievement_fields[14], "BadgeLockedURL", ""); + rc_json_get_optional_string(&achievement->badge_locked_url, response, &achievement_fields[14], "BadgeLockedURL", ""); if (!achievement->badge_locked_url[0]) - achievement->badge_locked_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, achievement->badge_name); + 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; if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) { achievement->author = last_author; } else { - if (!rc_json_get_required_string(&achievement->author, &response->response, &achievement_fields[6], "Author")) + if (!rc_json_get_required_string(&achievement->author, response, &achievement_fields[6], "Author")) return RC_MISSING_VALUE; if (achievement->author == NULL) { @@ -195,22 +181,23 @@ static int rc_api_process_fetch_game_data_achievements(rc_api_fetch_game_data_re } } - if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[8], "Created")) + if (!rc_json_get_required_unum(&timet, response, &achievement_fields[8], "Created")) return RC_MISSING_VALUE; achievement->created = (time_t)timet; - if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[9], "Modified")) + if (!rc_json_get_required_unum(&timet, response, &achievement_fields[9], "Modified")) return RC_MISSING_VALUE; achievement->updated = (time_t)timet; - achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD; - if (achievement_fields[10].value_end) { - len = achievement_fields[10].value_end - achievement_fields[10].value_start - 2; - if (len < sizeof(type) - 1) { - memcpy(type, achievement_fields[10].value_start + 1, len); - type[len] = '\0'; - achievement->type = rc_parse_achievement_type(type); - } - } + if (rc_json_field_string_matches(&achievement_fields[10], "")) + achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD; + else if (rc_json_field_string_matches(&achievement_fields[10], "progression")) + achievement->type = RC_ACHIEVEMENT_TYPE_PROGRESSION; + else if (rc_json_field_string_matches(&achievement_fields[10], "missable")) + achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE; + else if (rc_json_field_string_matches(&achievement_fields[10], "win_condition")) + achievement->type = RC_ACHIEVEMENT_TYPE_WIN; + else + achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD; /* legacy support : if title contains[m], change type to missable and remove[m] from title */ if (memcmp(achievement->title, "[m]", 3) == 0) { @@ -237,7 +224,7 @@ static int rc_api_process_fetch_game_data_achievements(rc_api_fetch_game_data_re return RC_OK; } -static int rc_api_process_fetch_game_data_leaderboards(rc_api_fetch_game_data_response_t* response, rc_api_leaderboard_definition_t* leaderboard, rc_json_field_t* array_field) { +static int rc_api_process_fetch_game_data_leaderboards(rc_api_response_t* response, rc_api_leaderboard_definition_t* leaderboard, rc_json_field_t* array_field) { rc_json_iterator_t iterator; size_t len; int result; @@ -258,13 +245,13 @@ static int rc_api_process_fetch_game_data_leaderboards(rc_api_fetch_game_data_re iterator.end = array_field->value_end; while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) { - if (!rc_json_get_required_unum(&leaderboard->id, &response->response, &leaderboard_fields[0], "ID")) + if (!rc_json_get_required_unum(&leaderboard->id, response, &leaderboard_fields[0], "ID")) return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&leaderboard->title, &response->response, &leaderboard_fields[1], "Title")) + if (!rc_json_get_required_string(&leaderboard->title, response, &leaderboard_fields[1], "Title")) return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&leaderboard->description, &response->response, &leaderboard_fields[2], "Description")) + if (!rc_json_get_required_string(&leaderboard->description, response, &leaderboard_fields[2], "Description")) return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&leaderboard->definition, &response->response, &leaderboard_fields[3], "Mem")) + if (!rc_json_get_required_string(&leaderboard->definition, response, &leaderboard_fields[3], "Mem")) return RC_MISSING_VALUE; rc_json_get_optional_bool(&result, &leaderboard_fields[5], "LowerIsBetter", 0); leaderboard->lower_is_better = (uint8_t)result; @@ -289,81 +276,6 @@ static int rc_api_process_fetch_game_data_leaderboards(rc_api_fetch_game_data_re return RC_OK; } -static int rc_api_process_fetch_game_data_subsets(rc_api_fetch_game_data_response_t* response, rc_api_subset_definition_t* subset, rc_json_field_t* subset_array_field) { - rc_json_iterator_t iterator; - rc_json_field_t array_field; - size_t len; - int result; - - rc_json_field_t subset_fields[] = { - RC_JSON_NEW_FIELD("GameAchievementSetID"), - RC_JSON_NEW_FIELD("SetTitle"), - RC_JSON_NEW_FIELD("ImageIcon"), - RC_JSON_NEW_FIELD("ImageIconURL"), - RC_JSON_NEW_FIELD("Achievements"), /* array */ - RC_JSON_NEW_FIELD("Leaderboards") /* array */ - }; - - memset(&iterator, 0, sizeof(iterator)); - iterator.json = subset_array_field->value_start; - iterator.end = subset_array_field->value_end; - - while (rc_json_get_array_entry_object(subset_fields, sizeof(subset_fields) / sizeof(subset_fields[0]), &iterator)) { - if (!rc_json_get_required_unum(&subset->id, &response->response, &subset_fields[0], "GameAchievementSetID")) - return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&subset->title, &response->response, &subset_fields[1], "SetTitle")) - return RC_MISSING_VALUE; - - /* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */ - rc_json_extract_filename(&subset_fields[2]); - rc_json_get_optional_string(&subset->image_name, &response->response, &subset_fields[2], "ImageIcon", ""); - - if (!rc_json_get_required_string(&subset->image_url, &response->response, &subset_fields[3], "ImageIconURL")) - return RC_MISSING_VALUE; - - /* estimate the amount of space necessary to store the achievements, and leaderboards. - determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation) - and add space for the structures. */ - len = (subset_fields[4].value_end - subset_fields[4].value_start) - /* achievements */ - subset_fields[4].array_size * (80 - sizeof(rc_api_achievement_definition_t)); - len += (subset_fields[5].value_end - subset_fields[5].value_start) - /* leaderboards */ - subset_fields[5].array_size * (60 - sizeof(rc_api_leaderboard_definition_t)); - - rc_buffer_reserve(&response->response.buffer, len); - /* end estimation */ - - if (!rc_json_get_required_array(&subset->num_achievements, &array_field, &response->response, &subset_fields[4], "Achievements")) - return RC_MISSING_VALUE; - - if (subset->num_achievements) { - subset->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_achievements * sizeof(rc_api_achievement_definition_t)); - if (!subset->achievements) - return RC_OUT_OF_MEMORY; - - result = rc_api_process_fetch_game_data_achievements(response, subset->achievements, &array_field); - if (result != RC_OK) - return result; - } - - if (!rc_json_get_required_array(&subset->num_leaderboards, &array_field, &response->response, &subset_fields[5], "Leaderboards")) - return RC_MISSING_VALUE; - - if (subset->num_leaderboards) { - subset->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_leaderboards * sizeof(rc_api_leaderboard_definition_t)); - if (!subset->leaderboards) - return RC_OUT_OF_MEMORY; - - result = rc_api_process_fetch_game_data_leaderboards(response, subset->leaderboards, &array_field); - if (result != RC_OK) - return result; - } - - ++subset; - } - - return RC_OK; -} - int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response) { rc_json_field_t array_field; size_t len; @@ -385,7 +297,6 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon RC_JSON_NEW_FIELD("RichPresencePatch"), RC_JSON_NEW_FIELD("Achievements"), /* array */ RC_JSON_NEW_FIELD("Leaderboards"), /* array */ - RC_JSON_NEW_FIELD("Sets") /* array */ }; memset(response, 0, sizeof(*response)); @@ -438,7 +349,7 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon if (!response->achievements) return RC_OUT_OF_MEMORY; - result = rc_api_process_fetch_game_data_achievements(response, response->achievements, &array_field); + result = rc_api_process_fetch_game_data_achievements(&response->response, response->achievements, &array_field); if (result != RC_OK) return result; } @@ -451,18 +362,194 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon if (!response->leaderboards) return RC_OUT_OF_MEMORY; - result = rc_api_process_fetch_game_data_leaderboards(response, response->leaderboards, &array_field); + result = rc_api_process_fetch_game_data_leaderboards(&response->response, response->leaderboards, &array_field); if (result != RC_OK) return result; } - rc_json_get_optional_array(&response->num_subsets, &array_field, &patchdata_fields[8], "Sets"); - if (response->num_subsets) { - response->subsets = (rc_api_subset_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_subsets * sizeof(rc_api_subset_definition_t)); - if (!response->subsets) + return RC_OK; +} + +void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch Game Sets --- */ + +int rc_api_init_fetch_game_sets_request(rc_api_request_t* request, const rc_api_fetch_game_sets_request_t* api_params) { + return rc_api_init_fetch_game_sets_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_game_sets_request_hosted(rc_api_request_t* request, + const rc_api_fetch_game_sets_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (!api_params->game_id && (!api_params->game_hash || !api_params->game_hash[0])) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "achievementsets", api_params->username, api_params->api_token)) { + if (api_params->game_id) + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + else + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +static int rc_api_process_fetch_game_sets_achievement_sets(rc_api_fetch_game_sets_response_t* response, + rc_api_achievement_set_definition_t* subset, + rc_json_field_t* subset_array_field) { + rc_json_iterator_t iterator; + rc_json_field_t array_field; + size_t len; + int result; + + rc_json_field_t subset_fields[] = { + RC_JSON_NEW_FIELD("AchievementSetId"), + RC_JSON_NEW_FIELD("GameId"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("Type"), + RC_JSON_NEW_FIELD("ImageIconUrl"), + RC_JSON_NEW_FIELD("Achievements"), /* array */ + RC_JSON_NEW_FIELD("Leaderboards") /* array */ + }; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = subset_array_field->value_start; + iterator.end = subset_array_field->value_end; + + while (rc_json_get_array_entry_object(subset_fields, sizeof(subset_fields) / sizeof(subset_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&subset->id, &response->response, &subset_fields[0], "AchievementSetId")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&subset->game_id, &response->response, &subset_fields[1], "GameId")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_string(&subset->title, &response->response, &subset_fields[2], "Title")) + return RC_MISSING_VALUE; + if (!subset->title || !subset->title[0]) + subset->title = response->title; + + if (rc_json_field_string_matches(&subset_fields[3], "core")) + subset->type = RC_ACHIEVEMENT_SET_TYPE_CORE; + else if (rc_json_field_string_matches(&subset_fields[3], "bonus")) + subset->type = RC_ACHIEVEMENT_SET_TYPE_BONUS; + else if (rc_json_field_string_matches(&subset_fields[3], "specialty")) + subset->type = RC_ACHIEVEMENT_SET_TYPE_SPECIALTY; + else if (rc_json_field_string_matches(&subset_fields[3], "exclusive")) + subset->type = RC_ACHIEVEMENT_SET_TYPE_EXCLUSIVE; + else + subset->type = RC_ACHIEVEMENT_SET_TYPE_BONUS; + + if (rc_json_field_string_matches(&subset_fields[4], response->image_url)) { + subset->image_url = response->image_url; + subset->image_name = response->image_name; + } + else { + if (!rc_json_get_required_string(&subset->image_url, &response->response, &subset_fields[4], "ImageIconUrl")) + return RC_MISSING_VALUE; + rc_json_extract_filename(&subset_fields[4]); + rc_json_get_optional_string(&subset->image_name, &response->response, &subset_fields[4], "ImageIconUrl", ""); + } + + /* estimate the amount of space necessary to store the achievements, and leaderboards. + determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation) + and add space for the structures. */ + len = (subset_fields[5].value_end - subset_fields[5].value_start) - /* achievements */ + subset_fields[5].array_size * (80 - sizeof(rc_api_achievement_definition_t)); + len += (subset_fields[6].value_end - subset_fields[6].value_start) - /* leaderboards */ + subset_fields[6].array_size * (60 - sizeof(rc_api_leaderboard_definition_t)); + + rc_buffer_reserve(&response->response.buffer, len); + /* end estimation */ + + if (!rc_json_get_required_array(&subset->num_achievements, &array_field, &response->response, &subset_fields[5], "Achievements")) + return RC_MISSING_VALUE; + + if (subset->num_achievements) { + subset->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_achievements * sizeof(rc_api_achievement_definition_t)); + if (!subset->achievements) + return RC_OUT_OF_MEMORY; + + result = rc_api_process_fetch_game_data_achievements(&response->response, subset->achievements, &array_field); + if (result != RC_OK) + return result; + } + + if (!rc_json_get_required_array(&subset->num_leaderboards, &array_field, &response->response, &subset_fields[6], "Leaderboards")) + return RC_MISSING_VALUE; + + if (subset->num_leaderboards) { + subset->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_leaderboards * sizeof(rc_api_leaderboard_definition_t)); + if (!subset->leaderboards) + return RC_OUT_OF_MEMORY; + + result = rc_api_process_fetch_game_data_leaderboards(&response->response, subset->leaderboards, &array_field); + if (result != RC_OK) + return result; + } + + ++subset; + } + + return RC_OK; +} + +int rc_api_process_fetch_game_sets_server_response(rc_api_fetch_game_sets_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t array_field; + 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("GameId"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("ConsoleId"), + RC_JSON_NEW_FIELD("ImageIconUrl"), + RC_JSON_NEW_FIELD("RichPresenceGameId"), + RC_JSON_NEW_FIELD("RichPresencePatch"), + RC_JSON_NEW_FIELD("Sets") /* array */ + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_unum(&response->id, &response->response, &fields[3], "GameId")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->title, &response->response, &fields[4], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->console_id, &response->response, &fields[5], "ConsoleId")) + return RC_MISSING_VALUE; + + rc_json_get_required_string(&response->image_url, &response->response, &fields[6], "ImageIconUrl"); + rc_json_extract_filename(&fields[6]); + rc_json_get_required_string(&response->image_name, &response->response, &fields[6], "ImageIconUrl"); + + rc_json_get_optional_unum(&response->session_game_id, &fields[7], "RichPresenceGameId", response->id); + + rc_json_get_optional_string(&response->rich_presence_script, &response->response, &fields[8], "RichPresencePatch", ""); + if (!response->rich_presence_script) + response->rich_presence_script = ""; + + rc_json_get_optional_array(&response->num_sets, &array_field, &fields[9], "Sets"); + if (response->num_sets) { + response->sets = (rc_api_achievement_set_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_sets * sizeof(rc_api_achievement_set_definition_t)); + if (!response->sets) return RC_OUT_OF_MEMORY; - result = rc_api_process_fetch_game_data_subsets(response, response->subsets, &array_field); + result = rc_api_process_fetch_game_sets_achievement_sets(response, response->sets, &array_field); if (result != RC_OK) return result; } @@ -470,7 +557,7 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon return RC_OK; } -void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response) { +void rc_api_destroy_fetch_game_sets_response(rc_api_fetch_game_sets_response_t* response) { rc_buffer_destroy(&response->response.buffer); } diff --git a/dep/rcheevos/src/rc_client.c b/dep/rcheevos/src/rc_client.c index 3665ff930..8d8824d0b 100644 --- a/dep/rcheevos/src/rc_client.c +++ b/dep/rcheevos/src/rc_client.c @@ -80,7 +80,7 @@ typedef struct rc_client_load_state_t } rc_client_load_state_t; static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state); -static void rc_client_begin_fetch_game_data(rc_client_load_state_t* callback_data); +static void rc_client_begin_fetch_game_sets(rc_client_load_state_t* callback_data); static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game); static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message); static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, const char* hash, const char* file_path); @@ -172,6 +172,7 @@ rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, client->state.hardcore = 1; client->state.required_unpaused_frames = RC_MINIMUM_UNPAUSED_FRAMES; + client->state.allow_background_memory_reads = 1; client->callbacks.read_memory = read_memory_function; client->callbacks.server_call = server_call_function; @@ -227,14 +228,6 @@ void rc_client_destroy(rc_client_t* client) /* ===== Logging ===== */ -static rc_client_t* g_hash_client = NULL; - -#ifdef RC_CLIENT_SUPPORTS_HASH -static void rc_client_log_hash_message(const char* message) { - rc_client_log_message(g_hash_client, message); -} -#endif - void rc_client_log_message(const rc_client_t* client, const char* message) { if (client->callbacks.log_call) @@ -699,7 +692,7 @@ static void rc_client_login_callback(const rc_api_server_response_t* server_resp login_callback_data->callback(result, error_message, client, login_callback_data->callback_userdata); if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN) - rc_client_begin_fetch_game_data(load_state); + rc_client_begin_fetch_game_sets(load_state); } else { client->user.username = rc_buffer_strcpy(&client->state.buffer, login_response.username); @@ -723,7 +716,7 @@ static void rc_client_login_callback(const rc_api_server_response_t* server_resp RC_CLIENT_LOG_INFO_FORMATTED(client, "%s logged in successfully", login_response.display_name); if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN) - rc_client_begin_fetch_game_data(load_state); + rc_client_begin_fetch_game_sets(load_state); if (login_callback_data->callback) login_callback_data->callback(RC_OK, NULL, client, login_callback_data->callback_userdata); @@ -918,51 +911,53 @@ int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], si return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, user->display_name); } -static void rc_client_subset_get_user_game_summary(const rc_client_subset_info_t* subset, - rc_client_user_game_summary_t* summary, const uint8_t unlock_bit) +static void rc_client_subset_get_user_game_summary(const rc_client_t* client, + const rc_client_subset_info_t* subset, rc_client_user_game_summary_t* summary) { rc_client_achievement_info_t* achievement = subset->achievements; rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; - uint32_t num_progression_achievements = 0, unlocked_progression_achievements = 0; - uint32_t num_win_condition_achievements = 0; - time_t last_progression_unlock = 0, first_win_condition_unlock = 0; - time_t last_achievement_unlock = 0; + time_t last_unlock_time = 0; + time_t last_progression_time = 0; + time_t first_win_time = 0; + int num_progression_achievements = 0; + int num_win_achievements = 0; + int num_unlocked_progression_achievements = 0; + const uint8_t unlock_bit = (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_mutex_lock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ + for (; achievement < stop; ++achievement) { switch (achievement->public_.category) { case RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE: ++summary->num_core_achievements; summary->points_core += achievement->public_.points; - if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION) - ++num_progression_achievements; - else if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_WIN) - ++num_win_condition_achievements; - if (achievement->public_.unlocked & unlock_bit) { ++summary->num_unlocked_achievements; summary->points_unlocked += achievement->public_.points; - last_achievement_unlock = (achievement->public_.unlock_time > last_achievement_unlock) ? - achievement->public_.unlock_time : - last_achievement_unlock; - if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION) - { - ++unlocked_progression_achievements; - last_progression_unlock = (achievement->public_.unlock_time > last_progression_unlock) ? - achievement->public_.unlock_time : - last_progression_unlock; + if (achievement->public_.unlock_time > last_unlock_time) + last_unlock_time = achievement->public_.unlock_time; + + if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION) { + ++num_unlocked_progression_achievements; + if (achievement->public_.unlock_time > last_progression_time) + last_progression_time = achievement->public_.unlock_time; } - else if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_WIN) - { - if (first_win_condition_unlock == 0 || - (achievement->public_.unlock_time > 0 && achievement->public_.unlock_time < first_win_condition_unlock)) - first_win_condition_unlock = achievement->public_.unlock_time; + else if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_WIN) { + if (first_win_time == 0 || achievement->public_.unlock_time < first_win_time) + first_win_time = achievement->public_.unlock_time; } } - if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) { + if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION) + ++num_progression_achievements; + else if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_WIN) + ++num_win_achievements; + + if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) ++summary->num_unsupported_achievements; - } break; @@ -975,25 +970,17 @@ static void rc_client_subset_get_user_game_summary(const rc_client_subset_info_t } } - /* Game considered beaten when all progression achievements are unlocked and any win condition achievement - * is unlocked, or all progression achievements are unlocked and there are no win condition achievements. */ - summary->beaten_time = 0; - if (unlocked_progression_achievements == num_progression_achievements && - (num_win_condition_achievements == 0 || first_win_condition_unlock > 0)) { - summary->beaten_time = (num_win_condition_achievements == 0) ? last_progression_unlock : first_win_condition_unlock; - } + rc_mutex_unlock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ - /* Game considered completed when all achievements are unlocked */ - summary->completed_time = 0; if (summary->num_unlocked_achievements == summary->num_core_achievements) - summary->completed_time = last_achievement_unlock; + summary->completed_time = last_unlock_time; + + if ((first_win_time || num_win_achievements == 0) && num_unlocked_progression_achievements == num_progression_achievements) + summary->beaten_time = (first_win_time > last_progression_time) ? first_win_time : last_progression_time; } void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary) { - const uint8_t unlock_bit = (client->state.hardcore) ? - RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; - if (!summary) return; @@ -1002,20 +989,47 @@ void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_g return; #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->get_user_game_summary) { - client->state.external_client->get_user_game_summary(summary); - return; + if (client->state.external_client) { + if (client->state.external_client->get_user_game_summary_v5) { + client->state.external_client->get_user_game_summary_v5(summary); + return; + } + if (client->state.external_client->get_user_game_summary) { + client->state.external_client->get_user_game_summary(summary); + return; + } } #endif - if (!rc_client_is_game_loaded(client)) + if (rc_client_is_game_loaded(client)) + rc_client_subset_get_user_game_summary(client, client->game->subsets, summary); +} + +void rc_client_get_user_subset_summary(const rc_client_t* client, uint32_t subset_id, rc_client_user_game_summary_t* summary) +{ + if (!summary) return; - rc_mutex_lock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ + memset(summary, 0, sizeof(*summary)); + if (!client || !subset_id) + return; - rc_client_subset_get_user_game_summary(client->game->subsets, summary, unlock_bit); +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->get_user_subset_summary) { + client->state.external_client->get_user_subset_summary(subset_id, summary); + return; + } +#endif - rc_mutex_unlock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ + if (rc_client_is_game_loaded(client)) { + const rc_client_subset_info_t* subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (subset->public_.id == subset_id) { + rc_client_subset_get_user_game_summary(client, subset, summary); + break; + } + } + } } typedef struct rc_client_fetch_all_user_progress_callback_data_t { @@ -1159,6 +1173,10 @@ static void rc_client_free_load_state(rc_client_load_state_t* load_state) free(load_state->start_session_response); } +#ifdef RC_CLIENT_SUPPORTS_HASH + rc_hash_destroy_iterator(&load_state->hash_iterator); +#endif + free(load_state); } @@ -1425,29 +1443,34 @@ static uint32_t rc_client_subset_toggle_hardcore_achievements(rc_client_subset_i break; } } - else if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE || - achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE) { + else { + achievement->public_.unlock_time = (active_bit == RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) ? + achievement->unlock_time_hardcore : achievement->unlock_time_softcore; - /* if it's active despite being unlocked, and we're in encore mode, leave it active */ - if (client->state.encore_mode) { - ++active_count; - continue; - } + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE || + achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE) { + /* if it's active despite being unlocked, and we're in encore mode, leave it active */ + if (client->state.encore_mode) { + ++active_count; + continue; + } - achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; - achievement->public_.unlock_time = (active_bit == RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) ? - achievement->unlock_time_hardcore : achievement->unlock_time_softcore; + /* switch to inactive */ + achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + + if (achievement->trigger && rc_trigger_state_active(achievement->trigger->state)) { + /* hide any active challenge indicators */ + if (achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) { + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + client_event.achievement = &achievement->public_; + client->callbacks.event_handler(&client_event, client); + } - if (achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) { - rc_client_event_t client_event; - memset(&client_event, 0, sizeof(client_event)); - client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; - client_event.achievement = &achievement->public_; - client->callbacks.event_handler(&client_event, client); + achievement->trigger->state = RC_TRIGGER_STATE_TRIGGERED; + } } - - if (achievement->trigger && rc_trigger_state_active(achievement->trigger->state)) - achievement->trigger->state = RC_TRIGGER_STATE_TRIGGERED; } } @@ -1669,6 +1692,11 @@ static void rc_client_free_pending_media(rc_client_pending_media_t* pending_medi free(pending_media); } +/* NOTE: address validation uses the read_memory callback to make sure the client + * will return data for the requested address. As such, this function must + * respect the `client->state.allow_background_memory_reads setting. Use + * rc_client_queue_activate_game to dispatch this function to the do_frame loop/ + */ static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_start_session_response_t *start_session_response) { rc_client_t* client = load_state->client; @@ -1718,11 +1746,11 @@ static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_s * client->state.load->game. since we've detached the load_state, this has to occur after * we've made the game active. */ if (pending_media->hash) { - rc_client_begin_change_media_from_hash(client, pending_media->hash, + rc_client_begin_change_media(client, pending_media->hash, pending_media->callback, pending_media->callback_userdata); } else { #ifdef RC_CLIENT_SUPPORTS_HASH - rc_client_begin_change_media(client, pending_media->file_path, + rc_client_begin_identify_and_change_media(client, pending_media->file_path, pending_media->data, pending_media->data_size, pending_media->callback, pending_media->callback_userdata); #endif @@ -1739,11 +1767,6 @@ static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_s /* if the game is still being loaded, make sure all the required memory addresses are accessible * so we can mark achievements as unsupported before loading them into the runtime. */ if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_ABORTED) { - /* TODO: it is desirable to not do memory reads from a background thread. Some emulators (like Dolphin) don't - * allow it. Dolphin's solution is to use a dummy read function that says all addresses are valid and - * switches to the actual read function after the callback is called. latter invalid reads will - * mark achievements as unsupported. */ - /* ASSERT: client->game must be set before calling this function so the read_memory callback can query the console_id */ rc_client_validate_addresses(load_state->game, client); @@ -1777,7 +1800,9 @@ static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_s if (load_state->hash->hash[0] != '[') { if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_LOCKED) { /* schedule the periodic ping */ - rc_client_scheduled_callback_data_t* callback_data = rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_scheduled_callback_data_t)); + rc_client_scheduled_callback_data_t* callback_data = (rc_client_scheduled_callback_data_t*) + rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_scheduled_callback_data_t)); + memset(callback_data, 0, sizeof(*callback_data)); callback_data->callback = rc_client_ping; callback_data->related_id = load_state->game->public_.id; @@ -1804,6 +1829,33 @@ static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_s rc_client_free_load_state(load_state); } +static void rc_client_dispatch_activate_game(struct rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data->data; + free(callback_data); + + (void)client; + (void)now; + + rc_client_activate_game(load_state, load_state->start_session_response); +} + +static void rc_client_queue_activate_game(rc_client_load_state_t* load_state) +{ + rc_client_scheduled_callback_data_t* scheduled_callback_data = + (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(rc_client_scheduled_callback_data_t)); + + if (!scheduled_callback_data) { + rc_client_load_error(load_state, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + + scheduled_callback_data->callback = rc_client_dispatch_activate_game; + scheduled_callback_data->data = load_state; + + rc_client_schedule_callback(load_state->client, scheduled_callback_data); +} + static void rc_client_start_session_callback(const rc_api_server_response_t* server_response, void* callback_data) { rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; @@ -1829,12 +1881,12 @@ static void rc_client_start_session_callback(const rc_api_server_response_t* ser outstanding_requests = rc_client_end_load_state(load_state); if (error_message) { - rc_client_load_error(callback_data, result, error_message); + rc_client_load_error(load_state, result, error_message); } else if (outstanding_requests < 0) { /* previous load state was aborted, load_state was free'd */ } - else if (outstanding_requests == 0) { + else if (outstanding_requests == 0 && load_state->client->state.allow_background_memory_reads) { rc_client_activate_game(load_state, &start_session_response); } else { @@ -1842,12 +1894,19 @@ static void rc_client_start_session_callback(const rc_api_server_response_t* ser (rc_api_start_session_response_t*)malloc(sizeof(rc_api_start_session_response_t)); if (!load_state->start_session_response) { - rc_client_load_error(callback_data, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + rc_client_load_error(load_state, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); } else { /* safer to parse the response again than to try to copy it */ rc_api_process_start_session_response(load_state->start_session_response, server_response->body); } + + if (outstanding_requests == 0) { + if (load_state->client->state.allow_background_memory_reads) + rc_client_activate_game(load_state, load_state->start_session_response); + else + rc_client_queue_activate_game(load_state); + } } rc_api_destroy_start_session_response(&start_session_response); @@ -1928,7 +1987,7 @@ static void rc_client_copy_achievements(rc_client_load_state_t* load_state, /* allocate the achievement array */ size = sizeof(rc_client_achievement_info_t) * num_achievements; - achievement = achievements = rc_buffer_alloc(buffer, size); + achievement = achievements = (rc_client_achievement_info_t*)rc_buffer_alloc(buffer, size); memset(achievements, 0, size); /* copy the achievement data */ @@ -1977,7 +2036,7 @@ static void rc_client_copy_achievements(rc_client_load_state_t* load_state, achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; } else { - rc_buffer_consume(buffer, preparse.parse.buffer, (uint8_t*)preparse.parse.buffer + preparse.parse.offset); + rc_buffer_consume(buffer, (const uint8_t*)preparse.parse.buffer, (uint8_t*)preparse.parse.buffer + preparse.parse.offset); } rc_destroy_preparse_state(&preparse); @@ -2070,7 +2129,7 @@ static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state, /* allocate the achievement array */ size = sizeof(rc_client_leaderboard_info_t) * num_leaderboards; buffer = &load_state->game->buffer; - leaderboard = leaderboards = rc_buffer_alloc(buffer, size); + leaderboard = leaderboards = (rc_client_leaderboard_info_t*)rc_buffer_alloc(buffer, size); memset(leaderboards, 0, size); /* copy the achievement data */ @@ -2120,7 +2179,7 @@ static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state, leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; } else { - rc_buffer_consume(buffer, preparse.parse.buffer, (uint8_t*)preparse.parse.buffer + preparse.parse.offset); + rc_buffer_consume(buffer, (const uint8_t*)preparse.parse.buffer, (uint8_t*)preparse.parse.buffer + preparse.parse.offset); } rc_destroy_preparse_state(&preparse); @@ -2133,10 +2192,10 @@ static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state, subset->leaderboards = leaderboards; } -static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* server_response, void* callback_data) +static void rc_client_fetch_game_sets_callback(const rc_api_server_response_t* server_response, void* callback_data) { rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; - rc_api_fetch_game_data_response_t fetch_game_data_response; + rc_api_fetch_game_sets_response_t fetch_game_sets_response; int outstanding_requests; const char* error_message; int result; @@ -2153,8 +2212,8 @@ static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* s return; } - result = rc_api_process_fetch_game_data_server_response(&fetch_game_data_response, server_response); - error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_game_data_response.response); + result = rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_game_sets_response.response); outstanding_requests = rc_client_end_load_state(load_state); @@ -2164,18 +2223,18 @@ static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* s else if (outstanding_requests < 0) { /* previous load state was aborted, load_state was free'd */ } - else if (fetch_game_data_response.id == 0) { + else if (fetch_game_sets_response.id == 0) { load_state->hash->game_id = 0; rc_client_process_resolved_hash(load_state); } else { rc_client_subset_info_t** next_subset; - rc_client_subset_info_t* core_subset; - uint32_t subset_index; + rc_client_subset_info_t* first_subset = NULL; + uint32_t set_index; /* hash exists outside the load state - always update it */ - load_state->hash->game_id = fetch_game_data_response.id; - RC_CLIENT_LOG_INFO_FORMATTED(load_state->client, "Identified game: %u \"%s\" (%s)", load_state->hash->game_id, fetch_game_data_response.title, load_state->hash->hash); + load_state->hash->game_id = fetch_game_sets_response.id; + RC_CLIENT_LOG_INFO_FORMATTED(load_state->client, "Identified game: %u \"%s\" (%s)", load_state->hash->game_id, fetch_game_sets_response.title, load_state->hash->hash); if (load_state->hash->hash[0] != '[') { /* not [NO HASH] or [SUBSETxx] */ @@ -2183,18 +2242,10 @@ static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* s load_state->game->public_.hash = load_state->hash->hash; } - core_subset = (rc_client_subset_info_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t)); - memset(core_subset, 0, sizeof(*core_subset)); - core_subset->public_.id = fetch_game_data_response.id; - core_subset->active = 1; - snprintf(core_subset->public_.badge_name, sizeof(core_subset->public_.badge_name), "%s", fetch_game_data_response.image_name); - core_subset->public_.badge_url = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_data_response.image_url); - load_state->subset = core_subset; - if (load_state->game->public_.console_id != RC_CONSOLE_UNKNOWN && - fetch_game_data_response.console_id != load_state->game->public_.console_id) { + fetch_game_sets_response.console_id != load_state->game->public_.console_id) { RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Data for game %u is for console %u, expecting console %u", - fetch_game_data_response.id, fetch_game_data_response.console_id, load_state->game->public_.console_id); + fetch_game_sets_response.id, fetch_game_sets_response.console_id, load_state->game->public_.console_id); } /* kick off the start session request while we process the game data */ @@ -2208,65 +2259,74 @@ static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* s } /* process the game data */ - rc_client_copy_achievements(load_state, core_subset, - fetch_game_data_response.achievements, fetch_game_data_response.num_achievements); - rc_client_copy_leaderboards(load_state, core_subset, - fetch_game_data_response.leaderboards, fetch_game_data_response.num_leaderboards); - - /* core set */ - rc_mutex_lock(&load_state->client->state.mutex); - load_state->game->public_.title = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_data_response.title); - load_state->game->subsets = core_subset; - load_state->game->public_.badge_name = core_subset->public_.badge_name; - load_state->game->public_.badge_url = core_subset->public_.badge_url; - load_state->game->public_.console_id = fetch_game_data_response.console_id; - rc_mutex_unlock(&load_state->client->state.mutex); - - core_subset->public_.title = load_state->game->public_.title; - - if (fetch_game_data_response.rich_presence_script && fetch_game_data_response.rich_presence_script[0]) { - result = rc_runtime_activate_richpresence(&load_state->game->runtime, fetch_game_data_response.rich_presence_script, NULL, 0); - if (result != RC_OK) { - RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing rich presence", result); - } - } - - next_subset = &core_subset->next; - for (subset_index = 0; subset_index < fetch_game_data_response.num_subsets; ++subset_index) { - rc_api_subset_definition_t* api_subset = &fetch_game_data_response.subsets[subset_index]; + next_subset = &first_subset; + for (set_index = 0; set_index < fetch_game_sets_response.num_sets; ++set_index) { + rc_api_achievement_set_definition_t* set = &fetch_game_sets_response.sets[set_index]; rc_client_subset_info_t* subset; subset = (rc_client_subset_info_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t)); memset(subset, 0, sizeof(*subset)); - subset->public_.id = api_subset->id; + subset->public_.id = set->id; subset->active = 1; - snprintf(subset->public_.badge_name, sizeof(subset->public_.badge_name), "%s", api_subset->image_name); - subset->public_.badge_url = rc_buffer_strcpy(&load_state->game->buffer, api_subset->image_url); - subset->public_.title = rc_buffer_strcpy(&load_state->game->buffer, api_subset->title); + snprintf(subset->public_.badge_name, sizeof(subset->public_.badge_name), "%s", set->image_name); + subset->public_.badge_url = rc_buffer_strcpy(&load_state->game->buffer, set->image_url); + subset->public_.title = rc_buffer_strcpy(&load_state->game->buffer, set->title); + + rc_client_copy_achievements(load_state, subset, set->achievements, set->num_achievements); + rc_client_copy_leaderboards(load_state, subset, set->leaderboards, set->num_leaderboards); + + if (set->type == RC_ACHIEVEMENT_SET_TYPE_CORE) { + if (!first_subset) + next_subset = &subset->next; + subset->next = first_subset; + first_subset = subset; + } + else { + *next_subset = subset; + next_subset = &subset->next; + } + } - rc_client_copy_achievements(load_state, subset, api_subset->achievements, api_subset->num_achievements); - rc_client_copy_leaderboards(load_state, subset, api_subset->leaderboards, api_subset->num_leaderboards); + if (!first_subset) { + rc_client_load_error(load_state, RC_NOT_FOUND, "Response contained no sets"); + } else { + load_state->subset = first_subset; - *next_subset = subset; - next_subset = &subset->next; - } + /* core set */ + rc_mutex_lock(&load_state->client->state.mutex); + load_state->game->public_.title = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_sets_response.title); + load_state->game->subsets = first_subset; + load_state->game->public_.badge_name = first_subset->public_.badge_name; + load_state->game->public_.badge_url = first_subset->public_.badge_url; + load_state->game->public_.console_id = fetch_game_sets_response.console_id; + rc_mutex_unlock(&load_state->client->state.mutex); - if (load_state->client->callbacks.post_process_game_data_response) { - load_state->client->callbacks.post_process_game_data_response(server_response, - &fetch_game_data_response, load_state->client, load_state->callback_userdata); + if (fetch_game_sets_response.rich_presence_script && fetch_game_sets_response.rich_presence_script[0]) { + result = rc_runtime_activate_richpresence(&load_state->game->runtime, fetch_game_sets_response.rich_presence_script, NULL, 0); + if (result != RC_OK) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing rich presence", result); + } + } + + if (load_state->client->callbacks.post_process_game_sets_response) { + load_state->client->callbacks.post_process_game_sets_response(server_response, + &fetch_game_sets_response, load_state->client, load_state->callback_userdata); + } } outstanding_requests = rc_client_end_load_state(load_state); if (outstanding_requests < 0) { /* previous load state was aborted, load_state was free'd */ } - else { - if (outstanding_requests == 0) + else if (outstanding_requests == 0) { + if (load_state->client->state.allow_background_memory_reads) rc_client_activate_game(load_state, load_state->start_session_response); + else + rc_client_queue_activate_game(load_state); } } - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); + rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); } static rc_client_game_info_t* rc_client_allocate_game(void) @@ -2438,6 +2498,8 @@ static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state) } } } + + rc_hash_destroy_iterator(&load_state->hash_iterator); /* done with this now */ #else load_state->game->public_.console_id = RC_CONSOLE_UNKNOWN; load_state->game->public_.hash = load_state->hash->hash; @@ -2479,9 +2541,6 @@ static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state) load_state->game->public_.hash = load_state->hash->hash; } - /* done with the hashing code, release the global pointer */ - g_hash_client = NULL; - #ifdef RC_CLIENT_SUPPORTS_EXTERNAL if (client->state.external_client) { if (client->state.external_client->add_game_hash) @@ -2495,7 +2554,7 @@ static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state) } #endif - rc_client_begin_fetch_game_data(load_state); + rc_client_begin_fetch_game_sets(load_state); } void rc_client_load_unknown_game(rc_client_t* client, const char* tried_hashes) @@ -2522,9 +2581,9 @@ void rc_client_load_unknown_game(rc_client_t* client, const char* tried_hashes) client->game = game; } -static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) +static void rc_client_begin_fetch_game_sets(rc_client_load_state_t* load_state) { - rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_api_fetch_game_sets_request_t fetch_game_sets_request; rc_client_t* client = load_state->client; rc_api_request_t request; int result; @@ -2533,6 +2592,8 @@ static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) result = client->state.user; if (result == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) load_state->progress = RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN; + else + load_state->progress = RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA; rc_mutex_unlock(&client->state.mutex); switch (result) { @@ -2548,26 +2609,26 @@ static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) return; } - memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); - fetch_game_data_request.username = client->user.username; - fetch_game_data_request.api_token = client->user.token; + memset(&fetch_game_sets_request, 0, sizeof(fetch_game_sets_request)); + fetch_game_sets_request.username = client->user.username; + fetch_game_sets_request.api_token = client->user.token; if (load_state->hash->is_unknown) /* lookup failed, but client provided a mapping */ - fetch_game_data_request.game_id = load_state->hash->game_id; + fetch_game_sets_request.game_id = load_state->hash->game_id; else - fetch_game_data_request.game_hash = load_state->hash->hash; + fetch_game_sets_request.game_hash = load_state->hash->hash; - result = rc_api_init_fetch_game_data_request_hosted(&request, &fetch_game_data_request, &client->state.host); + result = rc_api_init_fetch_game_sets_request_hosted(&request, &fetch_game_sets_request, &client->state.host); if (result != RC_OK) { rc_client_load_error(load_state, result, rc_error_str(result)); return; } rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME, 1); - RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Fetching data for hash %s", fetch_game_data_request.game_hash); + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Fetching data for hash %s", fetch_game_sets_request.game_hash); rc_client_begin_async(client, &load_state->async_handle); - client->callbacks.server_call(&request, rc_client_fetch_game_data_callback, load_state, client); + client->callbacks.server_call(&request, rc_client_fetch_game_sets_callback, load_state, client); rc_api_destroy_request(&request); } @@ -2633,7 +2694,7 @@ rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* } if (!game_hash) { - game_hash = rc_buffer_alloc(&client->state.buffer, sizeof(rc_client_game_hash_t)); + game_hash = (rc_client_game_hash_t*)rc_buffer_alloc(&client->state.buffer, sizeof(rc_client_game_hash_t)); memset(game_hash, 0, sizeof(*game_hash)); snprintf(game_hash->hash, sizeof(game_hash->hash), "%s", hash); game_hash->game_id = RC_CLIENT_UNKNOWN_GAME_ID; @@ -2739,7 +2800,7 @@ static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* loa } #endif else { - rc_client_begin_fetch_game_data(load_state); + rc_client_begin_fetch_game_sets(load_state); } return (client->state.load == load_state) ? &load_state->async_handle : NULL; @@ -2809,6 +2870,30 @@ rc_hash_iterator_t* rc_client_get_load_state_hash_iterator(rc_client_t* client) return NULL; } +static void rc_client_log_hash_message_verbose(const char* message, const rc_hash_iterator_t* iterator) +{ + const rc_client_load_state_t* load_state = (const rc_client_load_state_t*)iterator->userdata; + if (load_state->client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) + rc_client_log_message(load_state->client, message); +} + +static void rc_client_log_hash_message_error(const char* message, const rc_hash_iterator_t* iterator) +{ + const rc_client_load_state_t* load_state = (const rc_client_load_state_t*)iterator->userdata; + if (load_state->client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) + rc_client_log_message(load_state->client, message); +} + +void rc_client_set_hash_callbacks(rc_client_t* client, const struct rc_hash_callbacks* callbacks) +{ + memcpy(&client->callbacks.hash, callbacks, sizeof(*callbacks)); + + if (!callbacks->verbose_message) + client->callbacks.hash.verbose_message = rc_client_log_hash_message_verbose; + if (!callbacks->error_message) + client->callbacks.hash.error_message = rc_client_log_hash_message_error; +} + rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* client, uint32_t console_id, const char* file_path, const uint8_t* data, size_t data_size, @@ -2849,12 +2934,6 @@ rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* cl return NULL; } - if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { - g_hash_client = client; - rc_hash_init_error_message_callback(rc_client_log_hash_message); - rc_hash_init_verbose_message_callback(rc_client_log_hash_message); - } - if (!file_path) file_path = "?"; @@ -2867,9 +2946,18 @@ rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* cl load_state->callback = callback; load_state->callback_userdata = callback_userdata; - if (console_id == RC_CONSOLE_UNKNOWN) { - rc_hash_initialize_iterator(&load_state->hash_iterator, file_path, data, data_size); + /* initialize the iterator */ + rc_hash_initialize_iterator(&load_state->hash_iterator, file_path, data, data_size); + rc_hash_merge_callbacks(&load_state->hash_iterator, &client->callbacks.hash); + load_state->hash_iterator.userdata = load_state; + + if (!load_state->hash_iterator.callbacks.verbose_message) + load_state->hash_iterator.callbacks.verbose_message = rc_client_log_hash_message_verbose; + if (!load_state->hash_iterator.callbacks.error_message) + load_state->hash_iterator.callbacks.error_message = rc_client_log_hash_message_error; + /* calculate the hash */ + if (console_id == RC_CONSOLE_UNKNOWN) { if (!rc_hash_iterate(hash, &load_state->hash_iterator)) { rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); return NULL; @@ -2881,17 +2969,12 @@ rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* cl /* ASSERT: hash_iterator->index and hash_iterator->consoles[0] will be 0 from calloc */ load_state->hash_console_id = console_id; - if (data != NULL) { - if (!rc_hash_generate_from_buffer(hash, console_id, data, data_size)) { - rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); - return NULL; - } - } - else { - if (!rc_hash_generate_from_file(hash, console_id, file_path)) { - rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); - return NULL; - } + /* prevent initializing the iterator so it won't try other consoles in rc_client_process_resolved_hash */ + load_state->hash_iterator.index = 0; + + if (!rc_hash_generate(hash, console_id, &load_state->hash_iterator)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; } } @@ -3106,7 +3189,8 @@ static rc_client_async_handle_t* rc_client_begin_change_media_internal(rc_client rc_api_request_t request; int result; - if (game_hash->game_id != RC_CLIENT_UNKNOWN_GAME_ID) { + if (game_hash->game_id != RC_CLIENT_UNKNOWN_GAME_ID || /* previously looked up */ + game_hash->hash[0] == '[') { /* internal use - don't try to look up */ rc_client_change_media_internal(client, game_hash, callback, callback_userdata); return NULL; } @@ -3199,7 +3283,7 @@ static rc_client_game_info_t* rc_client_check_pending_media(rc_client_t* client, } /* still waiting for game data - don't call callback - it's queued */ - if (pending_media) + if (pending_media) return NULL; return game; @@ -3207,7 +3291,7 @@ static rc_client_game_info_t* rc_client_check_pending_media(rc_client_t* client, #ifdef RC_CLIENT_SUPPORTS_HASH -rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path, +rc_client_async_handle_t* rc_client_begin_identify_and_change_media(rc_client_t* client, const char* file_path, const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata) { rc_client_pending_media_t media; @@ -3227,9 +3311,9 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons } #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && !client->state.external_client->begin_change_media_from_hash) { - if (client->state.external_client->begin_change_media) - return client->state.external_client->begin_change_media(client, file_path, data, data_size, callback, callback_userdata); + if (client->state.external_client && !client->state.external_client->begin_change_media) { + if (client->state.external_client->begin_identify_and_change_media) + return client->state.external_client->begin_identify_and_change_media(client, file_path, data, data_size, callback, callback_userdata); } #endif @@ -3259,19 +3343,11 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons char hash[33]; int result; - if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { - g_hash_client = client; - rc_hash_init_error_message_callback(rc_client_log_hash_message); - rc_hash_init_verbose_message_callback(rc_client_log_hash_message); - } - if (data != NULL) result = rc_hash_generate_from_buffer(hash, game->public_.console_id, data, data_size); else result = rc_hash_generate_from_file(hash, game->public_.console_id, file_path); - g_hash_client = NULL; - if (!result) { /* when changing discs, if the disc is not supported by the system, allow it. this is * primarily for games that support user-provided audio CDs, but does allow using discs @@ -3292,8 +3368,8 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons if (!result) { #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->begin_change_media_from_hash) - return client->state.external_client->begin_change_media_from_hash(client, game_hash->hash, callback, callback_userdata); + if (client->state.external_client && client->state.external_client->begin_change_media) + return client->state.external_client->begin_change_media(client, game_hash->hash, callback, callback_userdata); #endif rc_client_change_media_internal(client, game_hash, callback, callback_userdata); @@ -3305,8 +3381,8 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons if (client->state.external_client) { if (client->state.external_client->add_game_hash) client->state.external_client->add_game_hash(game_hash->hash, game_hash->game_id); - if (client->state.external_client->begin_change_media_from_hash) - return client->state.external_client->begin_change_media_from_hash(client, game_hash->hash, callback, callback_userdata); + if (client->state.external_client->begin_change_media) + return client->state.external_client->begin_change_media(client, game_hash->hash, callback, callback_userdata); } #endif @@ -3315,7 +3391,7 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons #endif /* RC_CLIENT_SUPPORTS_HASH */ -rc_client_async_handle_t* rc_client_begin_change_media_from_hash(rc_client_t* client, const char* hash, +rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* hash, rc_client_callback_t callback, void* callback_userdata) { rc_client_pending_media_t media; @@ -3333,8 +3409,8 @@ rc_client_async_handle_t* rc_client_begin_change_media_from_hash(rc_client_t* cl } #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->begin_change_media_from_hash) { - return client->state.external_client->begin_change_media_from_hash(client, hash, callback, callback_userdata); + if (client->state.external_client && client->state.external_client->begin_change_media) { + return client->state.external_client->begin_change_media(client, hash, callback, callback_userdata); } #endif @@ -3364,7 +3440,7 @@ const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client) if (client->state.external_client->get_game_info) return rc_client_external_convert_v1_game(client, client->state.external_client->get_game_info()); - } + } #endif return client->game ? &client->game->public_ : NULL; @@ -3820,9 +3896,9 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli bucket_ptr->bucket_type = bucket_type; if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED) - qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_unlock_times); + qsort((void*)bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_unlock_times); else if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE) - qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_progress); + qsort((void*)bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_progress); ++bucket_ptr; } @@ -4144,7 +4220,7 @@ static void rc_client_award_achievement_callback(const rc_api_server_response_t* } static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data) -{ +{ rc_api_award_achievement_request_t api_params; rc_api_request_t request; int result; @@ -4298,7 +4374,7 @@ const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* if (leaderboard != NULL) return &leaderboard->public_; } - + return NULL; } @@ -4382,7 +4458,7 @@ rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* cli }; if (!client) - return calloc(1, sizeof(rc_client_leaderboard_list_t)); + return (rc_client_leaderboard_list_t*)calloc(1, list_size); #ifdef RC_CLIENT_SUPPORTS_EXTERNAL if (client->state.external_client && client->state.external_client->create_leaderboard_list) @@ -4390,7 +4466,7 @@ rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* cli #endif if (!client->game) - return calloc(1, sizeof(rc_client_leaderboard_list_t)); + return (rc_client_leaderboard_list_t*)calloc(1, list_size); memset(&bucket_counts, 0, sizeof(bucket_counts)); @@ -5234,6 +5310,19 @@ void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memo client->callbacks.read_memory = handler; } +void rc_client_set_allow_background_memory_reads(rc_client_t* client, int allowed) +{ + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_allow_background_memory_reads) + client->state.external_client->set_allow_background_memory_reads(allowed); +#endif + + client->state.allow_background_memory_reads = allowed; +} + static void rc_client_invalidate_processing_memref(rc_client_t* client) { /* if processing_memref is not set, this occurred following a pointer chain. ignore it. */ diff --git a/dep/rcheevos/src/rc_client_external.h b/dep/rcheevos/src/rc_client_external.h index 07dad7fef..fcf83ec76 100644 --- a/dep/rcheevos/src/rc_client_external.h +++ b/dep/rcheevos/src/rc_client_external.h @@ -35,6 +35,7 @@ typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_load_subse uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata); typedef const rc_client_game_t* (RC_CCONV *rc_client_external_get_game_info_func_t)(void); typedef const rc_client_subset_t* (RC_CCONV *rc_client_external_get_subset_info_func_t)(uint32_t subset_id); +typedef void (RC_CCONV* rc_client_external_get_user_subset_summary_func_t)(uint32_t subset_id, rc_client_user_game_summary_t* summary); typedef void (RC_CCONV *rc_client_external_get_user_game_summary_func_t)(rc_client_user_game_summary_t* summary); typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_change_media_func_t)(rc_client_t* client, const char* file_path, const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); @@ -99,8 +100,8 @@ typedef struct rc_client_external_t rc_client_external_get_subset_info_func_t get_subset_info; rc_client_external_action_func_t unload_game; rc_client_external_get_user_game_summary_func_t get_user_game_summary; - rc_client_external_begin_change_media_func_t begin_change_media; - rc_client_external_begin_load_game_func_t begin_change_media_from_hash; + rc_client_external_begin_change_media_func_t begin_identify_and_change_media; + rc_client_external_begin_load_game_func_t begin_change_media; rc_client_external_create_achievement_list_func_t create_achievement_list; rc_client_external_get_int_func_t has_achievements; @@ -136,9 +137,16 @@ typedef struct rc_client_external_t rc_client_external_get_achievement_info_func_t get_achievement_info_v3; rc_client_external_create_achievement_list_func_t create_achievement_list_v3; + /* VERSION 4 */ + rc_client_external_set_int_func_t set_allow_background_memory_reads; + + /* VERSION 5 */ + rc_client_external_get_user_game_summary_func_t get_user_game_summary_v5; + rc_client_external_get_user_subset_summary_func_t get_user_subset_summary; + } rc_client_external_t; -#define RC_CLIENT_EXTERNAL_VERSION 3 +#define RC_CLIENT_EXTERNAL_VERSION 5 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/dep/rcheevos/src/rc_client_external_versions.h b/dep/rcheevos/src/rc_client_external_versions.h index e8a324dba..90f6f6896 100644 --- a/dep/rcheevos/src/rc_client_external_versions.h +++ b/dep/rcheevos/src/rc_client_external_versions.h @@ -144,6 +144,28 @@ typedef struct v3_rc_client_achievement_list_info_t { rc_client_destroy_achievement_list_func_t destroy_func; } v3_rc_client_achievement_list_info_t; +/* user_game_summary */ + +typedef struct v1_rc_client_user_game_summary_t { + uint32_t num_core_achievements; + uint32_t num_unofficial_achievements; + uint32_t num_unlocked_achievements; + uint32_t num_unsupported_achievements; + uint32_t points_core; + uint32_t points_unlocked; +} v1_rc_client_user_game_summary_t; + +typedef struct v5_rc_client_user_game_summary_t { + uint32_t num_core_achievements; + uint32_t num_unofficial_achievements; + uint32_t num_unlocked_achievements; + uint32_t num_unsupported_achievements; + uint32_t points_core; + uint32_t points_unlocked; + time_t beaten_time; + time_t completed_time; +} v5_rc_client_user_game_summary_t; + RC_END_C_DECLS #endif /* RC_CLIENT_EXTERNAL_CONVERSIONS_H */ diff --git a/dep/rcheevos/src/rc_client_internal.h b/dep/rcheevos/src/rc_client_internal.h index 3f999386c..156c05c40 100644 --- a/dep/rcheevos/src/rc_client_internal.h +++ b/dep/rcheevos/src/rc_client_internal.h @@ -9,6 +9,9 @@ #ifdef RC_CLIENT_SUPPORTS_EXTERNAL #include "rc_client_external.h" #endif +#ifdef RC_CLIENT_SUPPORTS_HASH + #include "rhash/rc_hash_internal.h" +#endif #include "rc_compat.h" #include "rc_runtime.h" @@ -20,9 +23,9 @@ RC_BEGIN_C_DECLS | Callbacks | \*****************************************************************************/ -struct rc_api_fetch_game_data_response_t; -typedef void (RC_CCONV *rc_client_post_process_game_data_response_t)(const rc_api_server_response_t* server_response, - struct rc_api_fetch_game_data_response_t* game_data_response, rc_client_t* client, void* userdata); +struct rc_api_fetch_game_sets_response_t; +typedef void (RC_CCONV *rc_client_post_process_game_sets_response_t)(const rc_api_server_response_t* server_response, + struct rc_api_fetch_game_sets_response_t* game_sets_response, rc_client_t* client, void* userdata); typedef int (RC_CCONV *rc_client_can_submit_achievement_unlock_t)(uint32_t achievement_id, rc_client_t* client); typedef int (RC_CCONV *rc_client_can_submit_leaderboard_entry_t)(uint32_t leaderboard_id, rc_client_t* client); typedef int (RC_CCONV *rc_client_rich_presence_override_t)(rc_client_t* client, char buffer[], size_t buffersize); @@ -36,11 +39,15 @@ typedef struct rc_client_callbacks_t { rc_client_message_callback_t log_call; rc_get_time_millisecs_func_t get_time_millisecs; rc_client_identify_hash_func_t identify_unknown_hash; - rc_client_post_process_game_data_response_t post_process_game_data_response; + rc_client_post_process_game_sets_response_t post_process_game_sets_response; rc_client_can_submit_achievement_unlock_t can_submit_achievement_unlock; rc_client_can_submit_leaderboard_entry_t can_submit_leaderboard_entry; rc_client_rich_presence_override_t rich_presence_override; +#ifdef RC_CLIENT_SUPPORTS_HASH + rc_hash_callbacks_t hash; +#endif + void* client_data; } rc_client_callbacks_t; @@ -320,6 +327,7 @@ typedef struct rc_client_state_t { uint8_t user; uint8_t disconnect; uint8_t allow_leaderboards_in_softcore; + uint8_t allow_background_memory_reads; struct rc_client_load_state_t* load; struct rc_client_async_handle_t* async_handles[4]; diff --git a/dep/rcheevos/src/rc_client_types.natvis b/dep/rcheevos/src/rc_client_types.natvis index ddb71817f..397d400c3 100644 --- a/dep/rcheevos/src/rc_client_types.natvis +++ b/dep/rcheevos/src/rc_client_types.natvis @@ -289,7 +289,6 @@ {RC_CLIENT_LOAD_GAME_STATE_NONE} {RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME} {RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN} - {RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA} {RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION} {RC_CLIENT_LOAD_GAME_STATE_DONE} {RC_CLIENT_LOAD_GAME_STATE_ABORTED} diff --git a/dep/rcheevos/src/rc_version.h b/dep/rcheevos/src/rc_version.h index 4f8c25e36..bc8add301 100644 --- a/dep/rcheevos/src/rc_version.h +++ b/dep/rcheevos/src/rc_version.h @@ -7,8 +7,8 @@ RC_BEGIN_C_DECLS -#define RCHEEVOS_VERSION_MAJOR 11 -#define RCHEEVOS_VERSION_MINOR 6 +#define RCHEEVOS_VERSION_MAJOR 12 +#define RCHEEVOS_VERSION_MINOR 0 #define RCHEEVOS_VERSION_PATCH 0 #define RCHEEVOS_MAKE_VERSION(major, minor, patch) (major * 1000000 + minor * 1000 + patch) diff --git a/dep/rcheevos/src/rcheevos/condset.c b/dep/rcheevos/src/rcheevos/condset.c index 0875b35e6..21bab51ef 100644 --- a/dep/rcheevos/src/rcheevos/condset.c +++ b/dep/rcheevos/src/rcheevos/condset.c @@ -101,6 +101,11 @@ static int32_t rc_classify_conditions(rc_condset_t* self, const char* memaddr, c chain_length = 1; } while (*memaddr++ == '_'); + /* any combining conditions that don't actually feed into a non-combining condition + * need to still have space allocated for them. put them in "other" to match the + * logic in rc_find_next_classification */ + self->num_other_conditions += chain_length - 1; + return index; } @@ -338,8 +343,12 @@ rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse) { if (parse->buffer) { classification = rc_classify_condition(&condition); if (classification == RC_CONDITION_CLASSIFICATION_COMBINING) { - if (combining_classification == RC_CONDITION_CLASSIFICATION_COMBINING) - combining_classification = rc_find_next_classification(&(*memaddr)[1]); /* skip over '_' */ + if (combining_classification == RC_CONDITION_CLASSIFICATION_COMBINING) { + if (**memaddr == '_') + combining_classification = rc_find_next_classification(&(*memaddr)[1]); /* skip over '_' */ + else + combining_classification = RC_CONDITION_CLASSIFICATION_OTHER; + } classification = combining_classification; } diff --git a/dep/rcheevos/src/rcheevos/consoleinfo.c b/dep/rcheevos/src/rcheevos/consoleinfo.c index e25d74c5e..9277e9bad 100644 --- a/dep/rcheevos/src/rcheevos/consoleinfo.c +++ b/dep/rcheevos/src/rcheevos/consoleinfo.c @@ -724,10 +724,11 @@ static const rc_memory_regions_t rc_memory_regions_nes = { _rc_memory_regions_ne /* ===== Nintendo 64 ===== */ /* https://raw.githubusercontent.com/mikeryan/n64dev/master/docs/n64ops/n64ops%23h.txt */ +/* https://n64brew.dev/wiki/Memory_map#Virtual_Memory_Map */ static const rc_memory_region_t _rc_memory_regions_n64[] = { - { 0x000000U, 0x1FFFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 1 */ - { 0x200000U, 0x3FFFFFU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 2 */ - { 0x400000U, 0x7FFFFFU, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } /* expansion pak - cannot find any details for real address */ + { 0x000000U, 0x1FFFFFU, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 1 */ + { 0x200000U, 0x3FFFFFU, 0x80200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 2 */ + { 0x400000U, 0x7FFFFFU, 0x80400000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } /* expansion pak */ }; static const rc_memory_regions_t rc_memory_regions_n64 = { _rc_memory_regions_n64, 3 }; diff --git a/dep/rcheevos/src/rcheevos/memref.c b/dep/rcheevos/src/rcheevos/memref.c index 5b5d80cf8..f1e92102f 100644 --- a/dep/rcheevos/src/rcheevos/memref.c +++ b/dep/rcheevos/src/rcheevos/memref.c @@ -328,7 +328,7 @@ static float rc_build_float(uint32_t mantissa_bits, int32_t exponent, int sign) if (mantissa_bits == 0) { /* infinity */ #ifdef INFINITY /* INFINITY and NAN #defines require C99 */ - dbl = INFINITY; + dbl = (double)INFINITY; #else dbl = -log(0.0); #endif diff --git a/dep/rcheevos/src/rcheevos/operand.c b/dep/rcheevos/src/rcheevos/operand.c index e79050fa9..787c9c585 100644 --- a/dep/rcheevos/src/rcheevos/operand.c +++ b/dep/rcheevos/src/rcheevos/operand.c @@ -307,6 +307,9 @@ void rc_operand_set_float_const(rc_operand_t* self, double value) { } int rc_operands_are_equal(const rc_operand_t* left, const rc_operand_t* right) { + if (left == right) + return 1; + if (left->type != right->type) return 0; @@ -377,9 +380,6 @@ int rc_operand_is_float_memref(const rc_operand_t* self) { if (!rc_operand_is_memref(self)) return 0; - if (self->type == RC_OPERAND_RECALL) - return rc_memsize_is_float(self->memref_access_type); - if (self->value.memref->value.memref_type == RC_MEMREF_TYPE_MODIFIED_MEMREF) { const rc_modified_memref_t* memref = (const rc_modified_memref_t*)self->value.memref; if (memref->modifier_type != RC_OPERATOR_INDIRECT_READ) @@ -431,6 +431,9 @@ int rc_operand_is_float(const rc_operand_t* self) { if (self->type == RC_OPERAND_FP) return 1; + if (self->type == RC_OPERAND_RECALL) + return rc_memsize_is_float(self->size); + return rc_operand_is_float_memref(self); } diff --git a/dep/rcheevos/src/rcheevos/rc_runtime_types.natvis b/dep/rcheevos/src/rcheevos/rc_runtime_types.natvis index 45e31921d..25529c552 100644 --- a/dep/rcheevos/src/rcheevos/rc_runtime_types.natvis +++ b/dep/rcheevos/src/rcheevos/rc_runtime_types.natvis @@ -379,6 +379,8 @@ {RC_FORMAT_TENS} {RC_FORMAT_HUNDREDS} {RC_FORMAT_THOUSANDS} + {RC_FORMAT_UNSIGNED_VALUE} + {RC_FORMAT_UNFORMATTED} RC_FORMAT_STRING (101) RC_FORMAT_LOOKUP (102) RC_FORMAT_UNKNOWN_MACRO (103) diff --git a/dep/rcheevos/src/rcheevos/rc_validate.c b/dep/rcheevos/src/rcheevos/rc_validate.c index 6a5c658a7..7b7f085aa 100644 --- a/dep/rcheevos/src/rcheevos/rc_validate.c +++ b/dep/rcheevos/src/rcheevos/rc_validate.c @@ -174,8 +174,8 @@ static uint32_t rc_scale_value(uint32_t value, uint8_t oper, const rc_operand_t* case RC_OPERATOR_SUB: { - const uint32_t op_max = (operand->type == RC_OPERAND_CONST) ? operand->value.num : rc_max_value(operand); - if (value > op_max) + const uint32_t op_max = (operand->type == RC_OPERAND_CONST) ? operand->value.num : 0; + if (value >= op_max) return value - op_max; return 0xFFFFFFFF; @@ -351,11 +351,11 @@ static int rc_validate_condset_internal(const rc_condset_t* condset, char result snprintf(result, result_size, "Condition %d: Using pointer from previous frame", index); return 0; } - if (rc_operand_is_float(&cond->operand1) || rc_operand_is_float(&cond->operand2)) { + if (rc_operand_is_float(operand1) || rc_operand_is_float(&cond->operand2)) { snprintf(result, result_size, "Condition %d: Using non-integer value in AddAddress calcuation", index); return 0; } - if (rc_operand_type_is_transform(cond->operand1.type)) { + if (rc_operand_type_is_transform(operand1->type) && cond->oper != RC_OPERATOR_MULT) { snprintf(result, result_size, "Condition %d: Using transformed value in AddAddress calcuation", index); return 0; } @@ -721,7 +721,7 @@ static int rc_validate_comparison_overlap(int comparison1, uint32_t value1, int } static int rc_validate_conflicting_conditions(const rc_condset_t* conditions, const rc_condset_t* compare_conditions, - const char* prefix, const char* compare_prefix, char result[], const size_t result_size) + int has_hits, const char* prefix, const char* compare_prefix, char result[], const size_t result_size) { int comparison1, comparison2; uint32_t value1, value2; @@ -895,7 +895,15 @@ static int rc_validate_conflicting_conditions(const rc_condset_t* conditions, co { /* only ever report the redundancy on the non-ResetIf condition. The ResetIf is allowed to * fire when the non-ResetIf condition is not true. */ - continue; + if (has_hits) + continue; + } + else if (condition->type == RC_CONDITION_RESET_IF && compare_condition->type != RC_CONDITION_RESET_IF) + { + /* if the ResetIf condition is more restrictive than the non-ResetIf condition, + and there aren't any hits to clear, ignore it */ + if (has_hits) + continue; } else if (compare_condition->type == RC_CONDITION_MEASURED_IF || condition->type == RC_CONDITION_MEASURED_IF) { @@ -910,12 +918,11 @@ static int rc_validate_conflicting_conditions(const rc_condset_t* conditions, co continue; } } - else if (compare_condition->type == RC_CONDITION_TRIGGER || condition->type == RC_CONDITION_TRIGGER) + else if (condition->type == RC_CONDITION_TRIGGER && compare_condition->type != RC_CONDITION_TRIGGER) { /* Trigger is allowed to be redundant with non-trigger conditions as there may be limits that start a - * challenge that are furhter reduced for the completion of the challenge */ - if (compare_condition->type != condition->type) - continue; + * challenge that are further reduced for the completion of the challenge */ + continue; } break; @@ -948,12 +955,21 @@ static int rc_validate_trigger_internal(const rc_trigger_t* trigger, char result { const rc_condset_t* alt; int index; + int has_hits = (trigger->requirement && trigger->requirement->num_hittarget_conditions > 0); + if (!has_hits) { + for (alt = trigger->alternative; alt; alt = alt->next) { + if (alt->num_hittarget_conditions > 0) { + has_hits = 1; + break; + } + } + } if (!trigger->alternative) { if (!rc_validate_condset_internal(trigger->requirement, result, result_size, console_id, max_address)) return 0; - return rc_validate_conflicting_conditions(trigger->requirement, trigger->requirement, "", "", result, result_size); + return rc_validate_conflicting_conditions(trigger->requirement, trigger->requirement, has_hits, "", "", result, result_size); } snprintf(result, result_size, "Core "); @@ -961,7 +977,7 @@ static int rc_validate_trigger_internal(const rc_trigger_t* trigger, char result return 0; /* compare core to itself */ - if (!rc_validate_conflicting_conditions(trigger->requirement, trigger->requirement, "Core", "Core", result, result_size)) + if (!rc_validate_conflicting_conditions(trigger->requirement, trigger->requirement, has_hits, "Core", "Core", result, result_size)) return 0; index = 1; @@ -973,15 +989,15 @@ static int rc_validate_trigger_internal(const rc_trigger_t* trigger, char result /* compare alt to itself */ snprintf(altname, sizeof(altname), "Alt%d", index); - if (!rc_validate_conflicting_conditions(alt, alt, altname, altname, result, result_size)) + if (!rc_validate_conflicting_conditions(alt, alt, has_hits, altname, altname, result, result_size)) return 0; /* compare alt to core */ - if (!rc_validate_conflicting_conditions(trigger->requirement, alt, "Core", altname, result, result_size)) + if (!rc_validate_conflicting_conditions(trigger->requirement, alt, has_hits, "Core", altname, result, result_size)) return 0; /* compare core to alt */ - if (!rc_validate_conflicting_conditions(alt, trigger->requirement, altname, "Core", result, result_size)) + if (!rc_validate_conflicting_conditions(alt, trigger->requirement, has_hits, altname, "Core", result, result_size)) return 0; } diff --git a/dep/rcheevos/src/rcheevos/richpresence.c b/dep/rcheevos/src/rcheevos/richpresence.c index 46b093298..58c12340d 100644 --- a/dep/rcheevos/src/rcheevos/richpresence.c +++ b/dep/rcheevos/src/rcheevos/richpresence.c @@ -48,8 +48,9 @@ static void rc_alloc_helper_variable_memref_value(rc_richpresence_display_part_t condset = value->conditions; if (condset && !condset->next) { - /* single value - if it's only "measured" and "indirect" conditions, we can simplify to a memref */ - if (condset->num_measured_conditions && + /* single value - if it's a single Measured clause (including any AddSource/AddAddress helpers), we can + * simplify to a memref. If there are supporting clauses like MeasuredIf or ResetIf, we can't */ + if (condset->num_measured_conditions == 1 && !condset->num_pause_conditions && !condset->num_reset_conditions && !condset->num_other_conditions && !condset->num_hittarget_conditions) { rc_condition_t* condition = condset->conditions; @@ -537,6 +538,13 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, } else if (strncmp(line, "Format:", 7) == 0) { line += 7; + if (endline - line == 11 && memcmp(line, "Unformatted", 11) == 0) { + /* for backwards compatibility with the comma rollout, we allow old scripts + * to define an Unformatted type mapped to VALUE, and new versions will ignore + * the definition and use the built-in macro. skip the next line (FormatType=) */ + line = rc_parse_line(nextline, &endline, parse); + continue; + } lookup = RC_ALLOC_SCRATCH(rc_richpresence_lookup_t, parse); lookup->name = rc_alloc_str(parse, line, (int)(endline - line)); diff --git a/dep/rcheevos/src/rcheevos/runtime.c b/dep/rcheevos/src/rcheevos/runtime.c index 03af9de21..be92718b8 100644 --- a/dep/rcheevos/src/rcheevos/runtime.c +++ b/dep/rcheevos/src/rcheevos/runtime.c @@ -36,7 +36,7 @@ rc_runtime_t* rc_runtime_alloc(void) { rc_runtime_event_handler_t unused = &rc_runtime_natvis_helper; (void)unused; - self = malloc(sizeof(rc_runtime_t)); + self = (rc_runtime_t*)malloc(sizeof(rc_runtime_t)); if (self) { rc_runtime_init(self); @@ -307,7 +307,7 @@ int rc_runtime_activate_lboard(rc_runtime_t* self, uint32_t id, const char* mema rc_lboard_t* lboard; rc_preparse_state_t preparse; rc_runtime_lboard_t* runtime_lboard; - int size; + int32_t size; uint32_t i; (void)unused_L; @@ -808,7 +808,7 @@ void rc_runtime_invalidate_address(rc_runtime_t* self, uint32_t address) { } while (memref_list); } -void rc_runtime_validate_addresses(rc_runtime_t* self, rc_runtime_event_handler_t event_handler, +void rc_runtime_validate_addresses(rc_runtime_t* self, rc_runtime_event_handler_t event_handler, rc_runtime_validate_address_t validate_handler) { int num_invalid = 0; rc_memref_list_t* memref_list = &self->memrefs->memrefs; diff --git a/dep/rcheevos/src/rcheevos/runtime_progress.c b/dep/rcheevos/src/rcheevos/runtime_progress.c index dea567d6f..c87721eb4 100644 --- a/dep/rcheevos/src/rcheevos/runtime_progress.c +++ b/dep/rcheevos/src/rcheevos/runtime_progress.c @@ -560,8 +560,17 @@ static int rc_runtime_progress_read_variables(rc_runtime_progress_t* progress) } } + /* VS raises a C6385 warning here because it thinks count can exceed the size of the local_pending_variables array. + * When count is larger, pending_variables points to allocated memory, so the warning is wrong. */ +#if defined (_MSC_VER) + #pragma warning(push) + #pragma warning(disable:6385) +#endif while (count > 0) rc_reset_value(pending_variables[--count].variable); +#if defined (_MSC_VER) + #pragma warning(pop) +#endif if (pending_variables != local_pending_variables) free(pending_variables); @@ -911,7 +920,7 @@ uint32_t rc_runtime_progress_size(const rc_runtime_t* runtime, void* unused_L) int rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, void* unused_L) { - return rc_runtime_serialize_progress_sized(buffer, 0xFFFFFFFF, runtime, unused_L); + return rc_runtime_serialize_progress_sized((uint8_t*)buffer, 0xFFFFFFFF, runtime, unused_L); } int rc_runtime_serialize_progress_sized(uint8_t* buffer, uint32_t buffer_size, const rc_runtime_t* runtime, void* unused_L)