diff --git a/.editorconfig b/.editorconfig index f07212da..d1591493 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,7 +7,7 @@ end_of_line = crlf insert_final_newline = true # latin1 is a type of ASCII, should work with mbcs -[*.{h,cpp}] +[*.{h,c,cpp}] charset = latin1 indent_style = space indent_size = 2 diff --git a/include/rc_api_request.h b/include/rc_api_request.h index 64f25ec1..24bde81f 100644 --- a/include/rc_api_request.h +++ b/include/rc_api_request.h @@ -3,6 +3,8 @@ #include "rc_error.h" +#include + #ifdef __cplusplus extern "C" { #endif @@ -66,6 +68,15 @@ void rc_api_destroy_request(rc_api_request_t* request); void rc_api_set_host(const char* hostname); void rc_api_set_image_host(const char* hostname); +typedef struct rc_api_server_response_t { + /* Pointer to the data returned from the server */ + const char* body; + /* Length of data returned from the server (Content-Length) */ + size_t body_length; + /* HTTP status code returned from the server */ + int http_status_code; +} rc_api_server_response_t; + #ifdef __cplusplus } #endif diff --git a/include/rc_api_runtime.h b/include/rc_api_runtime.h index 68f56fd5..f0821f03 100644 --- a/include/rc_api_runtime.h +++ b/include/rc_api_runtime.h @@ -214,6 +214,8 @@ typedef struct rc_api_award_achievement_response_t { unsigned awarded_achievement_id; /* The updated player score */ unsigned new_player_score; + /* The updated player softcore score */ + unsigned new_player_score_softcore; /* The number of achievements the user has not yet unlocked for this game * (in hardcore/non-hardcore per hardcore flag in request) */ unsigned achievements_remaining; diff --git a/include/rc_api_user.h b/include/rc_api_user.h index 75884255..fc8e8074 100644 --- a/include/rc_api_user.h +++ b/include/rc_api_user.h @@ -33,6 +33,8 @@ typedef struct rc_api_login_response_t { const char* api_token; /* The current score of the player */ unsigned score; + /* The current softcore score of the player */ + unsigned score_softcore; /* The number of unread messages waiting for the player on the web site */ unsigned num_unread_messages; /* The preferred name to display for the player */ diff --git a/include/rc_client.h b/include/rc_client.h new file mode 100644 index 00000000..e1a561fc --- /dev/null +++ b/include/rc_client.h @@ -0,0 +1,582 @@ +#ifndef RC_CLIENT_H +#define RC_CLIENT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "rc_api_request.h" +#include "rc_error.h" + +#include +#include +#include + +/* implementation abstracted in rc_client_internal.h */ +typedef struct rc_client_t rc_client_t; +typedef struct rc_client_async_handle_t rc_client_async_handle_t; + +/*****************************************************************************\ +| Callbacks | +\*****************************************************************************/ + +/** + * Callback used to read num_bytes bytes from memory starting at address into buffer. + * Returns the number of bytes read. A return value of 0 indicates the address was invalid. + */ +typedef uint32_t (*rc_client_read_memory_func_t)(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client); + +/** + * Internal method passed to rc_client_server_call_t to process the server response. + */ +typedef void (*rc_client_server_callback_t)(const rc_api_server_response_t* server_response, void* callback_data); + +/** + * Callback used to issue a request to the server. + */ +typedef void (*rc_client_server_call_t)(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); + +/** + * Generic callback for asynchronous eventing. + */ +typedef void (*rc_client_callback_t)(int result, const char* error_message, rc_client_t* client, void* userdata); + +/** + * Callback for logging or displaying a message. + */ +typedef void (*rc_client_message_callback_t)(const char* message, const rc_client_t* client); + +/** + * Marks an async process as aborted. The associated callback will not be called. + */ +void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle); + +/*****************************************************************************\ +| Runtime | +\*****************************************************************************/ + +/** + * Creates a new rc_client_t object. + */ +rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function); + +/** + * Releases resources associated to a rc_client_t object. + * Pointer will no longer be valid after making this call. + */ +void rc_client_destroy(rc_client_t* client); + +/** + * Sets whether hardcore is enabled (on by default). + * Can be called with a game loaded. + * Enabling hardcore with a game loaded will raise an RC_CLIENT_EVENT_RESET + * event. Processing will be disabled until rc_client_reset is called. + */ +void rc_client_set_hardcore_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether hardcore is enabled (on by default). + */ +int rc_client_get_hardcore_enabled(const rc_client_t* client); + +/** + * Sets whether encore mode is enabled (off by default). + * Evaluated when loading a game. Has no effect while a game is loaded. + */ +void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether encore mode is enabled (off by default). + */ +int rc_client_get_encore_mode_enabled(const rc_client_t* client); + +/** + * Sets whether unofficial achievements should be loaded. + * Evaluated when loading a game. Has no effect while a game is loaded. + */ +void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether unofficial achievements should be loaded. + */ +int rc_client_get_unofficial_enabled(const rc_client_t* client); + +/** + * Sets whether spectator mode is enabled (off by default). + * If enabled, events for achievement unlocks and leaderboard submissions will be + * raised, but server calls to actually perform the unlock/submit will not occur. + * Can be modified while a game is loaded. Evaluated at unlock/submit time. + * Cannot be modified if disabled before a game is loaded. + */ +void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled); + +/** + * Gets whether spectator mode is enabled (off by default). + */ +int rc_client_get_spectator_mode_enabled(const rc_client_t* client); + +/** + * Attaches client-specific data to the runtime. + */ +void rc_client_set_userdata(rc_client_t* client, void* userdata); + +/** + * Gets the client-specific data attached to the runtime. + */ +void* rc_client_get_userdata(const rc_client_t* client); + +/** + * Sets the name of the server to use. + */ +void rc_client_set_host(const rc_client_t* client, const char* hostname); + +/*****************************************************************************\ +| Logging | +\*****************************************************************************/ + +/** + * Sets the logging level and provides a callback to be called to do the logging. + */ +void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback); +enum +{ + RC_CLIENT_LOG_LEVEL_NONE = 0, + RC_CLIENT_LOG_LEVEL_ERROR = 1, + RC_CLIENT_LOG_LEVEL_WARN = 2, + RC_CLIENT_LOG_LEVEL_INFO = 3, + RC_CLIENT_LOG_LEVEL_VERBOSE = 4 +}; + +/*****************************************************************************\ +| User | +\*****************************************************************************/ + +/** + * Attempt to login a user. + */ +rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client, + const char* username, const char* password, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Attempt to login a user. + */ +rc_client_async_handle_t* rc_client_begin_login_with_token(rc_client_t* client, + const char* username, const char* token, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Logout the user. + */ +void rc_client_logout(rc_client_t* logout); + +typedef struct rc_client_user_t { + const char* display_name; + const char* username; + const char* token; + uint32_t score; + uint32_t score_softcore; + uint32_t num_unread_messages; +} rc_client_user_t; + +/** + * Gets information about the logged in user. Will return NULL if the user is not logged in. + */ +const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client); + +/** + * Gets the URL for the user's profile picture. + * Returns RC_OK on success. + */ +int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size); + +typedef struct 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; +} rc_client_user_game_summary_t; + +/** + * Gets a breakdown of the number of achievements in the game, and how many the user has unlocked. + * Used for the "You have unlocked X of Y achievements" message shown when the game starts. + */ +void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary); + +/*****************************************************************************\ +| Game | +\*****************************************************************************/ + +/** + * Start loading an unidentified game. + */ +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, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Start loading a game. + */ +rc_client_async_handle_t* rc_client_begin_load_game(rc_client_t* client, const char* hash, + rc_client_callback_t callback, void* callback_userdata); + +/** + * Unloads the current game. + */ +void rc_client_unload_game(rc_client_t* client); + +typedef struct rc_client_game_t { + uint32_t id; + uint32_t console_id; + const char* title; + const char* hash; + const char* badge_name; +} rc_client_game_t; + +/** + * Get information about the current game. Returns NULL if no game is loaded. + */ +const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client); + +/** + * Gets the URL for the game image. + * Returns RC_OK on success. + */ +int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size); + +/** + * Changes the active disc in a multi-disc game. + */ +rc_client_async_handle_t* rc_client_begin_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); + +/*****************************************************************************\ +| Subsets | +\*****************************************************************************/ + +typedef struct rc_client_subset_t { + uint32_t id; + const char* title; + char badge_name[16]; + + uint32_t num_achievements; + uint32_t num_leaderboards; +} rc_client_subset_t; + +const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id); + +/*****************************************************************************\ +| Achievements | +\*****************************************************************************/ + +enum { + RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE = 0, /* unprocessed */ + RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE = 1, /* eligible to trigger */ + RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED = 2, /* earned by user */ + RC_CLIENT_ACHIEVEMENT_STATE_DISABLED = 3 /* not supported by this version of the runtime */ +}; + +enum { + RC_CLIENT_ACHIEVEMENT_CATEGORY_NONE = 0, + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE = (1 << 0), + RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL = (1 << 1), + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE | RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL +}; + +enum { + RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN = 0, + RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED = 1, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED = 2, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED = 3, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL = 4, + RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED = 5, + RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE = 6, + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE = 7 +}; + +enum { + RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE = 0, + RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE = (1 << 0), + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE = (1 << 1), + RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH = RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE | RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE +}; + +typedef struct rc_client_achievement_t { + const char* title; + const char* description; + char badge_name[8]; + char measured_progress[24]; + float measured_percent; + uint32_t id; + uint32_t points; + time_t unlock_time; + uint8_t state; + uint8_t category; + uint8_t bucket; + uint8_t unlocked; +} rc_client_achievement_t; + +/** + * Get information about an achievement. Returns NULL if not found. + */ +const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* client, uint32_t id); + +/** + * Gets the URL for the achievement image. + * Returns RC_OK on success. + */ +int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size); + +typedef struct rc_client_achievement_bucket_t { + rc_client_achievement_t** achievements; + uint32_t num_achievements; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} rc_client_achievement_bucket_t; + +typedef struct rc_client_achievement_list_t { + rc_client_achievement_bucket_t* buckets; + uint32_t num_buckets; +} rc_client_achievement_list_t; + +enum { + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE = 0, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS = 1 +}; + +/** + * Creates a list of achievements matching the specified category and grouping. + * Returns an allocated list that must be free'd by calling rc_client_destroy_achievement_list. + */ +rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping); + +/** + * Destroys a list allocated by rc_client_get_achievement_list. + */ +void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list); + +/*****************************************************************************\ +| Leaderboards | +\*****************************************************************************/ + +enum { + RC_CLIENT_LEADERBOARD_STATE_INACTIVE = 0, + RC_CLIENT_LEADERBOARD_STATE_ACTIVE = 1, + RC_CLIENT_LEADERBOARD_STATE_TRACKING = 2, + RC_CLIENT_LEADERBOARD_STATE_DISABLED = 3 +}; + +typedef struct rc_client_leaderboard_t { + const char* title; + const char* description; + const char* tracker_value; + uint32_t id; + uint8_t state; + uint8_t lower_is_better; +} rc_client_leaderboard_t; + +/** + * Get information about a leaderboard. Returns NULL if not found. + */ +const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id); + +typedef struct rc_client_leaderboard_tracker_t { + char display[24]; + uint32_t id; +} rc_client_leaderboard_tracker_t; + +typedef struct rc_client_leaderboard_bucket_t { + rc_client_leaderboard_t** leaderboards; + uint32_t num_leaderboards; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} rc_client_leaderboard_bucket_t; + +typedef struct rc_client_leaderboard_list_t { + rc_client_leaderboard_bucket_t* buckets; + uint32_t num_buckets; +} rc_client_leaderboard_list_t; + +enum { + RC_CLIENT_LEADERBOARD_BUCKET_UNKNOWN = 0, + RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE = 1, + RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE = 2, + RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED = 3, + RC_CLIENT_LEADERBOARD_BUCKET_ALL = 4 +}; + +enum { + RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE = 0, + RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING = 1 +}; + +/** + * Creates a list of leaderboards matching the specified grouping. + * Returns an allocated list that must be free'd by calling rc_client_destroy_leaderboard_list. + */ +rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping); + +/** + * Destroys a list allocated by rc_client_get_leaderboard_list. + */ +void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list); + +typedef struct rc_client_leaderboard_entry_t { + const char* user; + char display[24]; + time_t submitted; + uint32_t rank; + uint32_t index; +} rc_client_leaderboard_entry_t; + +typedef struct rc_client_leaderboard_entry_list_t { + rc_client_leaderboard_entry_t* entries; + uint32_t num_entries; + int32_t user_index; +} rc_client_leaderboard_entry_list_t; + +typedef void (*rc_client_fetch_leaderboard_entries_callback_t)(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata); + +/** + * Fetches a list of leaderboard entries from the server. + * Callback receives an allocated list that must be free'd by calling rc_client_destroy_leaderboard_entry_list. + */ +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id, + uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + +/** + * Fetches a list of leaderboard entries from the server containing the logged-in user. + * Callback receives an allocated list that must be free'd by calling rc_client_destroy_leaderboard_entry_list. + */ +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id, + uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata); + +/** + * Gets the URL for the profile picture of the user associated to a leaderboard entry. + * Returns RC_OK on success. + */ +int rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size); + +/** + * Destroys a list allocated by rc_client_begin_fetch_leaderboard_entries or rc_client_begin_fetch_leaderboard_entries_around_user. + */ +void rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list); + +/*****************************************************************************\ +| Rich Presence | +\*****************************************************************************/ + +/** + * Gets the current rich presence message. + * Returns the number of characters written to buffer. + */ +size_t rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size); + +/*****************************************************************************\ +| Processing | +\*****************************************************************************/ + +enum { + RC_CLIENT_EVENT_TYPE_NONE = 0, + RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED = 1, /* [achievement] was earned by the player */ + RC_CLIENT_EVENT_LEADERBOARD_STARTED = 2, /* [leaderboard] attempt has started */ + RC_CLIENT_EVENT_LEADERBOARD_FAILED = 3, /* [leaderboard] attempt failed */ + RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED = 4, /* [leaderboard] attempt submitted */ + RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW = 5, /* [achievement] challenge indicator should be shown */ + RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE = 6, /* [achievement] challenge indicator should be hidden */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW = 7, /* progress indicator should be shown for [achievement] for two seconds, replacing any currently visible progress indicator */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW = 8, /* [leaderboard_tracker] should be shown */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE = 9, /* [leaderboard_tracker] should be hidden */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE = 10, /* [leaderboard_tracker] updated */ + RC_CLIENT_EVENT_RESET = 11, /* emulated system should be reset (as the result of enabling hardcore) */ + RC_CLIENT_EVENT_GAME_COMPLETED = 12, /* all achievements for the game have been earned */ + RC_CLIENT_EVENT_SERVER_ERROR = 13 /* an API response returned a [server_error] and will not be retried */ +}; + +typedef struct rc_client_server_error_t +{ + const char* error_message; + const char* api; +} rc_client_server_error_t; + +typedef struct rc_client_event_t +{ + uint32_t type; + + rc_client_achievement_t* achievement; + rc_client_leaderboard_t* leaderboard; + rc_client_leaderboard_tracker_t* leaderboard_tracker; + rc_client_server_error_t* server_error; + +} rc_client_event_t; + +/** + * Callback used to notify the client when certain events occur. + */ +typedef void (*rc_client_event_handler_t)(const rc_client_event_t* event, rc_client_t* client); + +/** + * Provides a callback for event handling. + */ +void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler); + +/** + * Provides a callback for reading memory. + */ +void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler); + +/** + * Determines if there are any active achievements/leaderboards/rich presence that need processing. + */ +int rc_client_is_processing_required(rc_client_t* client); + +/** + * Processes achievements for the current frame. + */ +void rc_client_do_frame(rc_client_t* client); + +/** + * Processes the periodic queue. + * Called internally by rc_client_do_frame. + * Should be explicitly called if rc_client_do_frame is not being called because emulation is paused. + */ +void rc_client_idle(rc_client_t* client); + +/** + * Informs the runtime that the emulator has been reset. Will reset all achievements and leaderboards + * to their initial state (includes hiding indicators/trackers). + */ +void rc_client_reset(rc_client_t* client); + +/** + * Gets the number of bytes needed to serialized the runtime state. + */ +size_t rc_client_progress_size(rc_client_t* client); + +/** + * Serializes the runtime state into a buffer. + * Returns RC_OK on success, or an error indicator. + */ +int rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer); + +/** + * Deserializes the runtime state from a buffer. + * Returns RC_OK on success, or an error indicator. + */ +int rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_RUNTIME_H */ diff --git a/include/rc_consoles.h b/include/rc_consoles.h index 71e67764..0dd9c1ee 100644 --- a/include/rc_consoles.h +++ b/include/rc_consoles.h @@ -10,6 +10,7 @@ extern "C" { \*****************************************************************************/ enum { + RC_CONSOLE_UNKNOWN = 0, RC_CONSOLE_MEGA_DRIVE = 1, RC_CONSOLE_NINTENDO_64 = 2, RC_CONSOLE_SUPER_NINTENDO = 3, diff --git a/include/rc_error.h b/include/rc_error.h index 91a98c11..b80bc0ba 100644 --- a/include/rc_error.h +++ b/include/rc_error.h @@ -36,7 +36,13 @@ enum { RC_INVALID_MEASURED_TARGET = -23, RC_INVALID_COMPARISON = -24, RC_INVALID_STATE = -25, - RC_INVALID_JSON = -26 + RC_INVALID_JSON = -26, + RC_API_FAILURE = -27, + RC_LOGIN_REQUIRED = -28, + RC_NO_GAME_LOADED = -29, + RC_HARDCORE_DISABLED = -30, + RC_ABORTED = -31, + RC_NO_RESPONSE = -32 }; const char* rc_error_str(int ret); diff --git a/include/rc_hash.h b/include/rc_hash.h index 91ba5802..ba9ea1c0 100644 --- a/include/rc_hash.h +++ b/include/rc_hash.h @@ -27,20 +27,20 @@ extern "C" { /* data for rc_hash_iterate */ - struct rc_hash_iterator + typedef struct rc_hash_iterator { - uint8_t* buffer; + const uint8_t* buffer; size_t buffer_size; uint8_t consoles[12]; int index; const char* path; - }; + } rc_hash_iterator_t; /* initializes a rc_hash_iterator * - path must be provided * - if buffer and buffer_size are provided, path may be a filename (i.e. for something extracted from a zip file) */ - void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, uint8_t* buffer, size_t buffer_size); + void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, const uint8_t* buffer, size_t buffer_size); /* releases resources associated to a rc_hash_iterator */ diff --git a/src/rapi/rc_api_common.c b/src/rapi/rc_api_common.c index 683b99be..a9e410b8 100644 --- a/src/rapi/rc_api_common.c +++ b/src/rapi/rc_api_common.c @@ -242,6 +242,11 @@ int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_jso return RC_INVALID_STATE; #endif + if (!json || !*json) { + response->succeeded = 0; + return RC_NO_RESPONSE; + } + if (*json == '{') { int result = rc_json_parse_object(&json, fields, field_count, NULL); @@ -857,6 +862,18 @@ void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount) { return (void*)ptr; } +char* rc_buf_strncpy(rc_api_buffer_t* buffer, const char* src, size_t len) { + char* dst = rc_buf_reserve(buffer, len + 1); + memcpy(dst, src, len); + dst[len] = '\0'; + rc_buf_consume(buffer, dst, dst + len + 2); + return dst; +} + +char* rc_buf_strcpy(rc_api_buffer_t* buffer, const char* src) { + return rc_buf_strncpy(buffer, src, strlen(src)); +} + void rc_api_destroy_request(rc_api_request_t* request) { rc_buf_destroy(&request->buffer); } diff --git a/src/rapi/rc_api_common.h b/src/rapi/rc_api_common.h index fa18b779..4e0ec4c6 100644 --- a/src/rapi/rc_api_common.h +++ b/src/rapi/rc_api_common.h @@ -67,6 +67,8 @@ void rc_buf_destroy(rc_api_buffer_t* buffer); char* rc_buf_reserve(rc_api_buffer_t* buffer, size_t amount); void rc_buf_consume(rc_api_buffer_t* buffer, const char* start, char* end); void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount); +char* rc_buf_strcpy(rc_api_buffer_t* buffer, const char* src); +char* rc_buf_strncpy(rc_api_buffer_t* buffer, const char* src, size_t len); void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str); void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int value); diff --git a/src/rapi/rc_api_runtime.c b/src/rapi/rc_api_runtime.c index 43e72b79..879170da 100644 --- a/src/rapi/rc_api_runtime.c +++ b/src/rapi/rc_api_runtime.c @@ -350,6 +350,7 @@ int rc_api_process_award_achievement_response(rc_api_award_achievement_response_ RC_JSON_NEW_FIELD("Success"), RC_JSON_NEW_FIELD("Error"), RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("SoftcoreScore"), RC_JSON_NEW_FIELD("AchievementID"), RC_JSON_NEW_FIELD("AchievementsRemaining") }; @@ -375,8 +376,9 @@ int rc_api_process_award_achievement_response(rc_api_award_achievement_response_ } rc_json_get_optional_unum(&response->new_player_score, &fields[2], "Score", 0); - rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[3], "AchievementID", 0); - rc_json_get_optional_unum(&response->achievements_remaining, &fields[4], "AchievementsRemaining", (unsigned)-1); + rc_json_get_optional_unum(&response->new_player_score_softcore, &fields[3], "SoftcoreScore", 0); + rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[4], "AchievementID", 0); + rc_json_get_optional_unum(&response->achievements_remaining, &fields[5], "AchievementsRemaining", (unsigned)-1); return RC_OK; } diff --git a/src/rapi/rc_api_user.c b/src/rapi/rc_api_user.c index 546a8a05..1870e126 100644 --- a/src/rapi/rc_api_user.c +++ b/src/rapi/rc_api_user.c @@ -39,6 +39,7 @@ int rc_api_process_login_response(rc_api_login_response_t* response, const char* RC_JSON_NEW_FIELD("User"), RC_JSON_NEW_FIELD("Token"), RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("SoftcoreScore"), RC_JSON_NEW_FIELD("Messages"), RC_JSON_NEW_FIELD("DisplayName") }; @@ -56,9 +57,10 @@ int rc_api_process_login_response(rc_api_login_response_t* response, const char* return RC_MISSING_VALUE; rc_json_get_optional_unum(&response->score, &fields[4], "Score", 0); - rc_json_get_optional_unum(&response->num_unread_messages, &fields[5], "Messages", 0); + rc_json_get_optional_unum(&response->score_softcore, &fields[5], "SoftcoreScore", 0); + rc_json_get_optional_unum(&response->num_unread_messages, &fields[6], "Messages", 0); - rc_json_get_optional_string(&response->display_name, &response->response, &fields[6], "DisplayName", response->username); + rc_json_get_optional_string(&response->display_name, &response->response, &fields[7], "DisplayName", response->username); return RC_OK; } diff --git a/src/rcheevos/alloc.c b/src/rcheevos/alloc.c index c361eece..ac2a07bc 100644 --- a/src/rcheevos/alloc.c +++ b/src/rcheevos/alloc.c @@ -159,6 +159,17 @@ void rc_destroy_parse_state(rc_parse_state_t* parse) } } +unsigned rc_djb2(const char* input) +{ + unsigned result = 5381; + char c; + + while ((c = *input++) != '\0') + result = ((result << 5) + result) + c; /* result = result * 33 + c */ + + return result; +} + const char* rc_error_str(int ret) { switch (ret) { @@ -189,7 +200,12 @@ const char* rc_error_str(int ret) case RC_INVALID_COMPARISON: return "Invalid comparison"; case RC_INVALID_STATE: return "Invalid state"; case RC_INVALID_JSON: return "Invalid JSON"; - + case RC_API_FAILURE: return "API call failed"; + case RC_LOGIN_REQUIRED: return "Login required"; + case RC_NO_GAME_LOADED: return "No game loaded"; + case RC_HARDCORE_DISABLED: return "Hardcore disabled"; + case RC_ABORTED: return "Aborted"; + case RC_NO_RESPONSE: return "No response"; default: return "Unknown error"; } } diff --git a/src/rcheevos/compat.c b/src/rcheevos/compat.c index ce635fc1..0ba7601a 100644 --- a/src/rcheevos/compat.c +++ b/src/rcheevos/compat.c @@ -83,3 +83,57 @@ struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer) } #endif + +#ifndef RC_NO_THREADS +#ifdef _WIN32 + +/* https://gist.github.com/roxlu/1c1af99f92bafff9d8d9 */ + +#define WIN32_LEAN_AND_MEAN +#include + +void rc_mutex_init(rc_mutex_t* mutex) +{ + /* default security, not owned by calling thread, unnamed */ + mutex->handle = CreateMutex(NULL, FALSE, NULL); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + CloseHandle(mutex->handle); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + WaitForSingleObject(mutex->handle, 0xFFFFFFFF); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + ReleaseMutex(mutex->handle); +} + +#else + +void rc_mutex_init(rc_mutex_t* mutex) +{ + pthread_mutex_init(mutex, NULL); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + pthread_mutex_destroy(mutex); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + pthread_mutex_lock(mutex); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + pthread_mutex_unlock(mutex); +} + +#endif +#endif /* RC_NO_THREADS */ diff --git a/src/rcheevos/memref.c b/src/rcheevos/memref.c index 964f6849..fff2a6bc 100644 --- a/src/rcheevos/memref.c +++ b/src/rcheevos/memref.c @@ -387,7 +387,7 @@ char rc_memref_shared_size(char size) { return rc_memref_shared_sizes[index]; } -static unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* ud) { +unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* ud) { if (!peek) return 0; diff --git a/src/rcheevos/rc_client.c b/src/rcheevos/rc_client.c new file mode 100644 index 00000000..45f728de --- /dev/null +++ b/src/rcheevos/rc_client.c @@ -0,0 +1,4409 @@ +#include "rc_client_internal.h" + +#include "rc_api_info.h" +#include "rc_api_runtime.h" +#include "rc_api_user.h" +#include "rc_consoles.h" +#include "rc_internal.h" +#include "rc_hash.h" + +#include "../rapi/rc_api_common.h" + +#include + +#define RC_CLIENT_UNKNOWN_GAME_ID (uint32_t)-1 +#define RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS (10 * 60) /* ten minutes */ + +struct rc_client_async_handle_t { + uint8_t aborted; +}; + +typedef struct rc_client_generic_callback_data_t { + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + rc_client_async_handle_t async_handle; +} rc_client_generic_callback_data_t; + +typedef struct rc_client_pending_media_t +{ + const char* file_path; + uint8_t* data; + size_t data_size; + rc_client_callback_t callback; + void* callback_userdata; +} rc_client_pending_media_t; + +typedef struct rc_client_load_state_t +{ + rc_client_t* client; + rc_client_callback_t callback; + void* callback_userdata; + + rc_client_game_info_t* game; + rc_client_subset_info_t* subset; + rc_client_game_hash_t* hash; + + rc_hash_iterator_t hash_iterator; + rc_client_pending_media_t* pending_media; + + uint32_t* hardcore_unlocks; + uint32_t* softcore_unlocks; + uint32_t num_hardcore_unlocks; + uint32_t num_softcore_unlocks; + + rc_client_async_handle_t async_handle; + + uint8_t progress; + uint8_t outstanding_requests; + uint8_t hash_console_id; +} rc_client_load_state_t; + +static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message); +static void rc_client_begin_fetch_game_data(rc_client_load_state_t* callback_data); +static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, const char* hash, const char* file_path); +static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, time_t now); +static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset); +static void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard); + +/* ===== Construction/Destruction ===== */ + +static void rc_client_dummy_event_handler(const rc_client_event_t* event, rc_client_t* client) +{ +} + +rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function) +{ + rc_client_t* client = (rc_client_t*)calloc(1, sizeof(rc_client_t)); + if (!client) + return NULL; + + client->state.hardcore = 1; + + client->callbacks.read_memory = read_memory_function; + client->callbacks.server_call = server_call_function; + client->callbacks.event_handler = rc_client_dummy_event_handler; + rc_client_set_legacy_peek(client, RC_CLIENT_LEGACY_PEEK_AUTO); + + rc_mutex_init(&client->state.mutex); + + rc_buf_init(&client->state.buffer); + + return client; +} + +void rc_client_destroy(rc_client_t* client) +{ + if (!client) + return; + + rc_client_unload_game(client); + + rc_buf_destroy(&client->state.buffer); + + rc_mutex_destroy(&client->state.mutex); + + free(client); +} + +/* ===== Logging ===== */ + +static rc_client_t* g_hash_client = NULL; + +static void rc_client_log_hash_message(const char* message) { + rc_client_log_message(g_hash_client, message); +} + +void rc_client_log_message(const rc_client_t* client, const char* message) +{ + if (client->callbacks.log_call) + client->callbacks.log_call(message, client); +} + +static void rc_client_log_message_va(const rc_client_t* client, const char* format, va_list args) +{ + if (client->callbacks.log_call) { + char buffer[256]; + +#ifdef __STDC_WANT_SECURE_LIB__ + vsprintf_s(buffer, sizeof(buffer), format, args); +#else + vsprintf(buffer, format, args); +#endif + + client->callbacks.log_call(buffer, client); + } +} + +#ifdef RC_NO_VARIADIC_MACROS + +void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...) +{ + if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) { + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); + } +} + +#else + +void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...) +{ + va_list args; + va_start(args, format); + rc_client_log_message_va(client, format, args); + va_end(args); +} + +#endif /* RC_NO_VARIADIC_MACROS */ + +void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback) +{ + client->callbacks.log_call = callback; + client->state.log_level = callback ? level : RC_CLIENT_LOG_LEVEL_NONE; +} + +/* ===== Common ===== */ + +static int rc_client_async_handle_aborted(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + int aborted; + + rc_mutex_lock(&client->state.mutex); + aborted = async_handle->aborted; + rc_mutex_unlock(&client->state.mutex); + + return aborted; +} + +void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle) +{ + if (async_handle && client) { + rc_mutex_lock(&client->state.mutex); + async_handle->aborted = 1; + rc_mutex_unlock(&client->state.mutex); + } +} + +static const char* rc_client_server_error_message(int* result, int http_status_code, const rc_api_response_t* response) +{ + if (!response->succeeded) { + if (*result == RC_OK) { + *result = RC_API_FAILURE; + if (!response->error_message) + return "Unexpected API failure with no error message"; + } + + if (response->error_message) + return response->error_message; + } + + if (*result != RC_OK) + return rc_error_str(*result); + + return NULL; +} + +static void rc_client_raise_server_error_event(rc_client_t* client, const char* api, const char* error_message) +{ + rc_client_server_error_t server_error; + rc_client_event_t client_event; + + server_error.api = api; + server_error.error_message = error_message; + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_SERVER_ERROR; + client_event.server_error = &server_error; + + client->callbacks.event_handler(&client_event, client); +} + +static int rc_client_get_image_url(char buffer[], size_t buffer_size, int image_type, const char* image_name) +{ + rc_api_fetch_image_request_t image_request; + rc_api_request_t request; + int result; + + if (!buffer) + return RC_INVALID_STATE; + + memset(&image_request, 0, sizeof(image_request)); + image_request.image_type = image_type; + image_request.image_name = image_name; + result = rc_api_init_fetch_image_request(&request, &image_request); + if (result == RC_OK) + snprintf(buffer, buffer_size, "%s", request.url); + + rc_api_destroy_request(&request); + return result; +} + +/* ===== User ===== */ + +static void rc_client_login_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_generic_callback_data_t* login_callback_data = (rc_client_generic_callback_data_t*)callback_data; + rc_client_t* client = login_callback_data->client; + rc_api_login_response_t login_response; + rc_client_load_state_t* load_state; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(client, &login_callback_data->async_handle)) { + RC_CLIENT_LOG_VERBOSE(client, "Login aborted"); + free(login_callback_data); + return; + } + + if (client->state.user == RC_CLIENT_USER_STATE_NONE) { + /* logout was called */ + if (login_callback_data->callback) + login_callback_data->callback(RC_ABORTED, "Login aborted", client, login_callback_data->callback_userdata); + + free(login_callback_data); + return; + } + + result = rc_api_process_login_response(&login_response, server_response->body); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &login_response.response); + if (error_message) { + rc_mutex_lock(&client->state.mutex); + client->state.user = RC_CLIENT_USER_STATE_NONE; + load_state = client->state.load; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_ERR_FORMATTED(client, "Login failed: %s", error_message); + if (login_callback_data->callback) + login_callback_data->callback(result, error_message, client, login_callback_data->callback_userdata); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_begin_fetch_game_data(load_state); + } + else { + client->user.username = rc_buf_strcpy(&client->state.buffer, login_response.username); + + if (strcmp(login_response.username, login_response.display_name) == 0) + client->user.display_name = client->user.username; + else + client->user.display_name = rc_buf_strcpy(&client->state.buffer, login_response.display_name); + + client->user.token = rc_buf_strcpy(&client->state.buffer, login_response.api_token); + client->user.score = login_response.score; + client->user.score_softcore = login_response.score_softcore; + client->user.num_unread_messages = login_response.num_unread_messages; + + rc_mutex_lock(&client->state.mutex); + client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; + load_state = client->state.load; + rc_mutex_unlock(&client->state.mutex); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "%s logged in successfully", login_response.display_name); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_begin_fetch_game_data(load_state); + + if (login_callback_data->callback) + login_callback_data->callback(RC_OK, NULL, client, login_callback_data->callback_userdata); + } + + rc_api_destroy_login_response(&login_response); + free(login_callback_data); +} + +static rc_client_async_handle_t* rc_client_begin_login(rc_client_t* client, + const rc_api_login_request_t* login_request, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_generic_callback_data_t* callback_data; + rc_api_request_t request; + int result = rc_api_init_login_request(&request, login_request); + const char* error_message = rc_error_str(result); + + if (result == RC_OK) { + rc_mutex_lock(&client->state.mutex); + + if (client->state.user == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) { + error_message = "Login already in progress"; + result = RC_INVALID_STATE; + } + client->state.user = RC_CLIENT_USER_STATE_LOGIN_REQUESTED; + + rc_mutex_unlock(&client->state.mutex); + } + + if (result != RC_OK) { + callback(result, error_message, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_generic_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + + client->callbacks.server_call(&request, rc_client_login_callback, callback_data, client); + rc_api_destroy_request(&request); + + return &callback_data->async_handle; +} + +rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client, + const char* username, const char* password, rc_client_callback_t callback, void* callback_userdata) +{ + rc_api_login_request_t login_request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!username || !username[0]) { + callback(RC_INVALID_STATE, "username is required", client, callback_userdata); + return NULL; + } + + if (!password || !password[0]) { + callback(RC_INVALID_STATE, "password is required", client, callback_userdata); + return NULL; + } + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = username; + login_request.password = password; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with password)", username); + return rc_client_begin_login(client, &login_request, callback, callback_userdata); +} + +rc_client_async_handle_t* rc_client_begin_login_with_token(rc_client_t* client, + const char* username, const char* token, rc_client_callback_t callback, void* callback_userdata) +{ + rc_api_login_request_t login_request; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!username || !username[0]) { + callback(RC_INVALID_STATE, "username is required", client, callback_userdata); + return NULL; + } + + if (!token || !token[0]) { + callback(RC_INVALID_STATE, "token is required", client, callback_userdata); + return NULL; + } + + memset(&login_request, 0, sizeof(login_request)); + login_request.username = username; + login_request.api_token = token; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with token)", username); + return rc_client_begin_login(client, &login_request, callback, callback_userdata); +} + +void rc_client_logout(rc_client_t* client) +{ + rc_client_load_state_t* load_state; + + if (!client) + return; + + switch (client->state.user) { + case RC_CLIENT_USER_STATE_LOGGED_IN: + RC_CLIENT_LOG_INFO_FORMATTED(client, "Logging %s out", client->user.display_name); + break; + + case RC_CLIENT_USER_STATE_LOGIN_REQUESTED: + RC_CLIENT_LOG_INFO(client, "Aborting login"); + break; + } + + rc_mutex_lock(&client->state.mutex); + + client->state.user = RC_CLIENT_USER_STATE_NONE; + memset(&client->user, 0, sizeof(client->user)); + + load_state = client->state.load; + + rc_mutex_unlock(&client->state.mutex); + + rc_client_unload_game(client); + + if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN) + rc_client_load_error(load_state, RC_ABORTED, "Login aborted"); +} + +const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client) +{ + return (client && client->state.user == RC_CLIENT_USER_STATE_LOGGED_IN) ? &client->user : NULL; +} + +int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size) +{ + if (!user) + return RC_INVALID_STATE; + + 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) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public.num_achievements; + 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.unlocked & unlock_bit) { + ++summary->num_unlocked_achievements; + summary->points_unlocked += achievement->public.points; + } + else if (achievement->public.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) { + ++summary->num_unsupported_achievements; + } + + break; + + case RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL: + ++summary->num_unofficial_achievements; + break; + + default: + continue; + } + } +} + +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; + + memset(summary, 0, sizeof(*summary)); + if (!client || !client->game) + return; + + rc_mutex_lock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ + + rc_client_subset_get_user_game_summary(client->game->subsets, summary, unlock_bit); + + rc_mutex_unlock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ +} + +/* ===== Game ===== */ + +static void rc_client_free_game(rc_client_game_info_t* game) +{ + rc_runtime_destroy(&game->runtime); + + rc_buf_destroy(&game->buffer); + + free(game); +} + +static void rc_client_free_load_state(rc_client_load_state_t* load_state) +{ + if (load_state->game) + rc_client_free_game(load_state->game); + + if (load_state->hardcore_unlocks) + free(load_state->hardcore_unlocks); + if (load_state->softcore_unlocks) + free(load_state->softcore_unlocks); + + free(load_state); +} + +static void rc_client_begin_load_state(rc_client_load_state_t* load_state, uint8_t state, uint8_t num_requests) +{ + rc_mutex_lock(&load_state->client->state.mutex); + + load_state->progress = state; + load_state->outstanding_requests += num_requests; + + rc_mutex_unlock(&load_state->client->state.mutex); +} + +static int rc_client_end_load_state(rc_client_load_state_t* load_state) +{ + int remaining_requests = 0; + int aborted = 0; + + rc_mutex_lock(&load_state->client->state.mutex); + + if (load_state->outstanding_requests > 0) + --load_state->outstanding_requests; + remaining_requests = load_state->outstanding_requests; + + if (load_state->client->state.load != load_state) + aborted = 1; + + rc_mutex_unlock(&load_state->client->state.mutex); + + if (aborted) { + /* we can't actually free the load_state itself if there are any outstanding requests + * or their callbacks will try to use the free'd memory. As they call end_load_state, + * the outstanding_requests count will reach zero and the memory will be free'd then. */ + if (remaining_requests == 0) { + /* if one of the callbacks called rc_client_load_error, progress will be set to + * RC_CLIENT_LOAD_STATE_UNKNOWN. There's no need to call the callback with RC_ABORTED + * in that case, as it will have already been called with something more appropriate. */ + if (load_state->progress != RC_CLIENT_LOAD_STATE_UNKNOWN_GAME && load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", load_state->client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + } + + return -1; + } + + return remaining_requests; +} + +static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message) +{ + int remaining_requests = 0; + + rc_mutex_lock(&load_state->client->state.mutex); + + load_state->progress = RC_CLIENT_LOAD_STATE_UNKNOWN_GAME; + if (load_state->client->state.load == load_state) + load_state->client->state.load = NULL; + + remaining_requests = load_state->outstanding_requests; + + rc_mutex_unlock(&load_state->client->state.mutex); + + if (load_state->callback) + load_state->callback(result, error_message, load_state->client, load_state->callback_userdata); + + /* we can't actually free the load_state itself if there are any outstanding requests + * or their callbacks will try to use the free'd memory. as they call end_load_state, + * the outstanding_requests count will reach zero and the memory will be free'd then. */ + if (remaining_requests == 0) + rc_client_free_load_state(load_state); +} + +static void rc_client_load_aborted(rc_client_load_state_t* load_state) +{ + /* prevent callback from being called when manually aborted */ + load_state->callback = NULL; + + /* mark the game as no longer being loaded */ + rc_client_load_error(load_state, RC_ABORTED, NULL); + + /* decrement the async counter and potentially free the load_state object */ + rc_client_end_load_state(load_state); +} + +static void rc_client_invalidate_memref_achievements(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref) +{ + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public.state == RC_CLIENT_ACHIEVEMENT_STATE_DISABLED) + continue; + + if (rc_trigger_contains_memref(achievement->trigger, memref)) { + achievement->public.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled achievement %u. Invalid address %06X", achievement->public.id, memref->address); + } + } + } +} + +static void rc_client_invalidate_memref_leaderboards(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref) +{ + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED) + continue; + + if (rc_trigger_contains_memref(&leaderboard->lboard->start, memref)) + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_trigger_contains_memref(&leaderboard->lboard->cancel, memref)) + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_trigger_contains_memref(&leaderboard->lboard->submit, memref)) + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else if (rc_value_contains_memref(&leaderboard->lboard->value, memref)) + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + else + continue; + + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled leaderboard %u. Invalid address %06X", leaderboard->public.id, memref->address); + } + } +} + +static void rc_client_validate_addresses(rc_client_game_info_t* game, rc_client_t* client) +{ + const rc_memory_regions_t* regions = rc_console_memory_regions(game->public.console_id); + const uint32_t max_address = (regions && regions->num_regions > 0) ? + regions->region[regions->num_regions - 1].end_address : 0xFFFFFFFF; + uint8_t buffer[8]; + uint32_t total_count = 0; + uint32_t invalid_count = 0; + + rc_memref_t** last_memref = &game->runtime.memrefs; + rc_memref_t* memref = game->runtime.memrefs; + for (; memref; memref = memref->next) { + if (!memref->value.is_indirect) { + total_count++; + + if (memref->address > max_address || + client->callbacks.read_memory(memref->address, buffer, 1, client) == 0) { + /* invalid address, remove from chain so we don't have to evaluate it in the future. + * it's still there, so anything referencing it will always fetch 0. */ + *last_memref = memref->next; + + rc_client_invalidate_memref_achievements(game, client, memref); + rc_client_invalidate_memref_leaderboards(game, client, memref); + + invalid_count++; + continue; + } + } + + last_memref = &memref->next; + } + + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "%u/%u memory addresses valid", total_count - invalid_count, total_count); +} + +static void rc_client_update_legacy_runtime_achievements(rc_client_game_info_t* game, uint32_t active_count) +{ + if (active_count > 0) { + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* stop; + rc_runtime_trigger_t* trigger; + + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public.num_achievements; + + if (active_count <= game->runtime.trigger_capacity) { + if (active_count != 0) + memset(game->runtime.triggers, 0, active_count * sizeof(rc_runtime_trigger_t)); + } + else { + if (game->runtime.triggers) + free(game->runtime.triggers); + + game->runtime.trigger_capacity = active_count; + game->runtime.triggers = (rc_runtime_trigger_t*)calloc(1, active_count * sizeof(rc_runtime_trigger_t)); + if (!game->runtime.triggers) { + /* Unexpected, no callback available, just fail */ + break; + } + } + + trigger = game->runtime.triggers; + achievement = subset->achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { + trigger->id = achievement->public.id; + memcpy(trigger->md5, achievement->md5, 16); + trigger->trigger = achievement->trigger; + ++trigger; + } + } + } + } + + game->runtime.trigger_count = active_count; +} + +static uint32_t rc_client_subset_count_active_achievements(const rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public.num_achievements; + uint32_t active_count = 0; + + for (; achievement < stop; ++achievement) { + if (achievement->public.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + ++active_count; + } + + return active_count; +} + +static void rc_client_update_active_achievements(rc_client_game_info_t* game) +{ + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + active_count += rc_client_subset_count_active_achievements(subset); + } + + rc_client_update_legacy_runtime_achievements(game, active_count); +} + +static uint32_t rc_client_subset_toggle_hardcore_achievements(rc_client_subset_info_t* subset, rc_client_t* client, uint8_t active_bit) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public.num_achievements; + uint32_t active_count = 0; + + for (; achievement < stop; ++achievement) { + if ((achievement->public.unlocked & active_bit) == 0) { + switch (achievement->public.state) { + case RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED: + case RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE: + rc_reset_trigger(achievement->trigger); + achievement->public.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE; + ++active_count; + break; + + case RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE: + ++active_count; + break; + } + } + else 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; + + 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); + } + } + } + + return active_count; +} + +static void rc_client_toggle_hardcore_achievements(rc_client_game_info_t* game, rc_client_t* client, uint8_t active_bit) +{ + uint32_t active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (subset->active) + active_count += rc_client_subset_toggle_hardcore_achievements(subset, client, active_bit); + } + + rc_client_update_legacy_runtime_achievements(game, active_count); +} + +static void rc_client_activate_achievements(rc_client_game_info_t* game, rc_client_t* client) +{ + const uint8_t active_bit = (client->state.encore_mode) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE : (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_client_toggle_hardcore_achievements(game, client, active_bit); +} + +static void rc_client_activate_leaderboards(rc_client_game_info_t* game, rc_client_t* client) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + unsigned active_count = 0; + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + switch (leaderboard->public.state) { + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + if (client->state.hardcore) { + rc_reset_lboard(leaderboard->lboard); + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + ++active_count; + } + break; + + default: + if (client->state.hardcore) + ++active_count; + else + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + break; + } + } + } + + if (active_count > 0) { + rc_runtime_lboard_t* lboard; + + if (active_count <= game->runtime.lboard_capacity) { + if (active_count != 0) + memset(game->runtime.lboards, 0, active_count * sizeof(rc_runtime_lboard_t)); + } + else { + if (game->runtime.lboards) + free(game->runtime.lboards); + + game->runtime.lboard_capacity = active_count; + game->runtime.lboards = (rc_runtime_lboard_t*)calloc(1, active_count * sizeof(rc_runtime_lboard_t)); + } + + lboard = game->runtime.lboards; + + subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public.state == RC_CLIENT_LEADERBOARD_STATE_ACTIVE || + leaderboard->public.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) { + lboard->id = leaderboard->public.id; + memcpy(lboard->md5, leaderboard->md5, 16); + lboard->lboard = leaderboard->lboard; + ++lboard; + } + } + } + } + + game->runtime.lboard_count = active_count; +} + +static void rc_client_deactivate_leaderboards(rc_client_game_info_t* game, rc_client_t* client) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + + rc_client_subset_info_t* subset = game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + switch (leaderboard->public.state) { + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + rc_client_release_leaderboard_tracker(client->game, leaderboard); + /* fallthrough to default */ + default: + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE; + break; + } + } + } + + game->runtime.lboard_count = 0; +} + +static void rc_client_apply_unlocks(rc_client_subset_info_t* subset, uint32_t* unlocks, uint32_t num_unlocks, uint8_t mode) +{ + rc_client_achievement_info_t* start = subset->achievements; + rc_client_achievement_info_t* stop = start + subset->public.num_achievements; + rc_client_achievement_info_t* scan; + unsigned i; + + for (i = 0; i < num_unlocks; ++i) { + uint32_t id = unlocks[i]; + for (scan = start; scan < stop; ++scan) { + if (scan->public.id == id) { + scan->public.unlocked |= mode; + + if (scan == start) + ++start; + else if (scan + 1 == stop) + --stop; + break; + } + } + } +} + +static void rc_client_activate_game(rc_client_load_state_t* load_state) +{ + rc_client_t* client = load_state->client; + + rc_mutex_lock(&client->state.mutex); + load_state->progress = (client->state.load == load_state) ? + RC_CLIENT_LOAD_STATE_DONE : RC_CLIENT_LOAD_STATE_UNKNOWN_GAME; + client->state.load = NULL; + rc_mutex_unlock(&client->state.mutex); + + if (load_state->progress != RC_CLIENT_LOAD_STATE_DONE) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else if ((!load_state->softcore_unlocks || !load_state->hardcore_unlocks) && + client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) { + /* unlocks not available - assume malloc failed */ + if (load_state->callback) + load_state->callback(RC_INVALID_STATE, "Unlock arrays were not allocated", client, load_state->callback_userdata); + } + else { + if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) { + rc_client_apply_unlocks(load_state->subset, load_state->softcore_unlocks, + load_state->num_softcore_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + rc_client_apply_unlocks(load_state->subset, load_state->hardcore_unlocks, + load_state->num_hardcore_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + } + + rc_mutex_lock(&client->state.mutex); + if (client->state.load == NULL) + client->game = load_state->game; + rc_mutex_unlock(&client->state.mutex); + + if (client->game != load_state->game) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else { + /* if a change media request is pending, kick it off */ + rc_client_pending_media_t* pending_media; + + rc_mutex_lock(&load_state->client->state.mutex); + pending_media = load_state->pending_media; + load_state->pending_media = NULL; + rc_mutex_unlock(&load_state->client->state.mutex); + + if (pending_media) { + rc_client_begin_change_media(client, pending_media->file_path, + pending_media->data, pending_media->data_size, pending_media->callback, pending_media->callback_userdata); + if (pending_media->data) + free(pending_media->data); + free((void*)pending_media->file_path); + free(pending_media); + } + + /* client->game must be set before calling this function so it can query the console_id */ + rc_client_validate_addresses(load_state->game, client); + + rc_client_activate_achievements(load_state->game, client); + rc_client_activate_leaderboards(load_state->game, client); + + 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_buf_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; + callback_data->when = time(NULL) + 30; + rc_client_schedule_callback(client, callback_data); + } + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Game %u loaded, hardcode %s%s", load_state->game->public.id, + client->state.hardcore ? "enabled" : "disabled", + (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) ? ", spectating" : ""); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Subset %u loaded", load_state->subset->public.id); + } + + if (load_state->callback) + load_state->callback(RC_OK, NULL, client, load_state->callback_userdata); + + /* detach the game object so it doesn't get freed by free_load_state */ + load_state->game = NULL; + } + } + + rc_client_free_load_state(load_state); +} + +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; + rc_api_start_session_response_t start_session_response; + int outstanding_requests; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(load_state->client, &load_state->async_handle)) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while starting session"); + return; + } + + result = rc_api_process_start_session_response(&start_session_response, server_response->body); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &start_session_response.response); + outstanding_requests = rc_client_end_load_state(load_state); + + if (error_message) { + rc_client_load_error(callback_data, result, error_message); + } + else if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + if (outstanding_requests == 0) + rc_client_activate_game(load_state); + } + + rc_api_destroy_start_session_response(&start_session_response); +} + +static void rc_client_unlocks_callback(const rc_api_server_response_t* server_response, void* callback_data, int mode) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; + rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response; + int outstanding_requests; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(load_state->client, &load_state->async_handle)) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while fetching unlocks"); + return; + } + + result = rc_api_process_fetch_user_unlocks_response(&fetch_user_unlocks_response, server_response->body); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_user_unlocks_response.response); + outstanding_requests = rc_client_end_load_state(load_state); + + if (error_message) { + rc_client_load_error(callback_data, result, error_message); + } + else if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + if (mode == RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) { + const size_t array_size = fetch_user_unlocks_response.num_achievement_ids * sizeof(uint32_t); + load_state->num_hardcore_unlocks = fetch_user_unlocks_response.num_achievement_ids; + load_state->hardcore_unlocks = (uint32_t*)malloc(array_size); + if (load_state->hardcore_unlocks) + memcpy(load_state->hardcore_unlocks, fetch_user_unlocks_response.achievement_ids, array_size); + } + else { + const size_t array_size = fetch_user_unlocks_response.num_achievement_ids * sizeof(uint32_t); + load_state->num_softcore_unlocks = fetch_user_unlocks_response.num_achievement_ids; + load_state->softcore_unlocks = (uint32_t*)malloc(array_size); + if (load_state->softcore_unlocks) + memcpy(load_state->softcore_unlocks, fetch_user_unlocks_response.achievement_ids, array_size); + } + + if (outstanding_requests == 0) + rc_client_activate_game(load_state); + } + + rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response); +} + +static void rc_client_hardcore_unlocks_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_unlocks_callback(server_response, callback_data, RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE); +} + +static void rc_client_softcore_unlocks_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_unlocks_callback(server_response, callback_data, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); +} + +static void rc_client_begin_start_session(rc_client_load_state_t* load_state) +{ + rc_api_start_session_request_t start_session_params; + rc_api_fetch_user_unlocks_request_t unlock_params; + rc_client_t* client = load_state->client; + rc_api_request_t start_session_request; + rc_api_request_t hardcore_unlock_request; + rc_api_request_t softcore_unlock_request; + int result; + + memset(&start_session_params, 0, sizeof(start_session_params)); + start_session_params.username = client->user.username; + start_session_params.api_token = client->user.token; + start_session_params.game_id = load_state->hash->game_id; + + result = rc_api_init_start_session_request(&start_session_request, &start_session_params); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + } + else { + memset(&unlock_params, 0, sizeof(unlock_params)); + unlock_params.username = client->user.username; + unlock_params.api_token = client->user.token; + unlock_params.game_id = load_state->hash->game_id; + unlock_params.hardcore = 1; + + result = rc_api_init_fetch_user_unlocks_request(&hardcore_unlock_request, &unlock_params); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + } + else { + unlock_params.hardcore = 0; + + result = rc_api_init_fetch_user_unlocks_request(&softcore_unlock_request, &unlock_params); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + } + else { + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_STARTING_SESSION, 3); + + /* TODO: create single server request to do all three of these */ + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Starting session for game %u", start_session_params.game_id); + client->callbacks.server_call(&start_session_request, rc_client_start_session_callback, load_state, client); + client->callbacks.server_call(&hardcore_unlock_request, rc_client_hardcore_unlocks_callback, load_state, client); + client->callbacks.server_call(&softcore_unlock_request, rc_client_softcore_unlocks_callback, load_state, client); + + rc_api_destroy_request(&softcore_unlock_request); + } + + rc_api_destroy_request(&hardcore_unlock_request); + } + + rc_api_destroy_request(&start_session_request); + } +} + +static void rc_client_copy_achievements(rc_client_load_state_t* load_state, + rc_client_subset_info_t* subset, + const rc_api_achievement_definition_t* achievement_definitions, uint32_t num_achievements) +{ + const rc_api_achievement_definition_t* read; + const rc_api_achievement_definition_t* stop; + rc_client_achievement_info_t* achievements; + rc_client_achievement_info_t* achievement; + rc_api_buffer_t* buffer; + rc_parse_state_t parse; + const char* memaddr; + size_t size; + int trigger_size; + + subset->achievements = NULL; + subset->public.num_achievements = num_achievements; + + if (num_achievements == 0) + return; + + stop = achievement_definitions + num_achievements; + + /* if not testing unofficial, filter them out */ + if (!load_state->client->state.unofficial_enabled) { + for (read = achievement_definitions; read < stop; ++read) { + if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) + --num_achievements; + } + + subset->public.num_achievements = num_achievements; + + if (num_achievements == 0) + return; + } + + /* preallocate space for achievements */ + size = 24 /* assume average title length of 24 */ + + 48 /* assume average description length of 48 */ + + sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2 /* trigger container */ + + sizeof(rc_condition_t) * 8 /* assume average trigger length of 8 conditions */ + + sizeof(rc_client_achievement_info_t); + rc_buf_reserve(&load_state->game->buffer, size * num_achievements); + + /* allocate the achievement array */ + size = sizeof(rc_client_achievement_info_t) * num_achievements; + buffer = &load_state->game->buffer; + achievement = achievements = rc_buf_alloc(buffer, size); + memset(achievements, 0, size); + + /* copy the achievement data */ + for (read = achievement_definitions; read < stop; ++read) { + if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE && !load_state->client->state.unofficial_enabled) + continue; + + achievement->public.title = rc_buf_strcpy(buffer, read->title); + achievement->public.description = rc_buf_strcpy(buffer, read->description); + snprintf(achievement->public.badge_name, sizeof(achievement->public.badge_name), "%s", read->badge_name); + achievement->public.id = read->id; + achievement->public.points = read->points; + achievement->public.category = (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) ? + RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; + + memaddr = read->definition; + rc_runtime_checksum(memaddr, achievement->md5); + + trigger_size = rc_trigger_size(memaddr); + if (trigger_size < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", trigger_size, read->id); + achievement->public.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + } + else { + /* populate the item, using the communal memrefs pool */ + rc_init_parse_state(&parse, rc_buf_reserve(buffer, trigger_size), NULL, 0); + parse.first_memref = &load_state->game->runtime.memrefs; + parse.variables = &load_state->game->runtime.variables; + achievement->trigger = RC_ALLOC(rc_trigger_t, &parse); + rc_parse_trigger_internal(achievement->trigger, &memaddr, &parse); + + if (parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", parse.offset, read->id); + achievement->public.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; + achievement->public.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; + } + else { + rc_buf_consume(buffer, parse.buffer, (char*)parse.buffer + parse.offset); + achievement->trigger->memrefs = NULL; /* memrefs managed by runtime */ + } + + rc_destroy_parse_state(&parse); + } + + ++achievement; + } + + subset->achievements = achievements; +} + +static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state, + rc_client_subset_info_t* subset, + const rc_api_leaderboard_definition_t* leaderboard_definitions, uint32_t num_leaderboards) +{ + const rc_api_leaderboard_definition_t* read; + const rc_api_leaderboard_definition_t* stop; + rc_client_leaderboard_info_t* leaderboards; + rc_client_leaderboard_info_t* leaderboard; + rc_api_buffer_t* buffer; + rc_parse_state_t parse; + const char* memaddr; + const char* ptr; + size_t size; + int lboard_size; + + subset->leaderboards = NULL; + subset->public.num_leaderboards = num_leaderboards; + + if (num_leaderboards == 0) + return; + + /* preallocate space for achievements */ + size = 24 /* assume average title length of 24 */ + + 48 /* assume average description length of 48 */ + + sizeof(rc_lboard_t) /* lboard container */ + + (sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2) * 3 /* start/submit/cancel */ + + (sizeof(rc_value_t) + sizeof(rc_condset_t)) /* value */ + + sizeof(rc_condition_t) * 4 * 4 /* assume average of 4 conditions in each start/submit/cancel/value */ + + sizeof(rc_client_leaderboard_info_t); + rc_buf_reserve(&load_state->game->buffer, size * num_leaderboards); + + /* allocate the achievement array */ + size = sizeof(rc_client_leaderboard_info_t) * num_leaderboards; + buffer = &load_state->game->buffer; + leaderboard = leaderboards = rc_buf_alloc(buffer, size); + memset(leaderboards, 0, size); + + /* copy the achievement data */ + read = leaderboard_definitions; + stop = read + num_leaderboards; + do { + leaderboard->public.title = rc_buf_strcpy(buffer, read->title); + leaderboard->public.description = rc_buf_strcpy(buffer, read->description); + leaderboard->public.id = read->id; + leaderboard->public.lower_is_better = read->lower_is_better; + leaderboard->format = (uint8_t)read->format; + + memaddr = read->definition; + rc_runtime_checksum(memaddr, leaderboard->md5); + + ptr = strstr(memaddr, "VAL:"); + if (ptr != NULL) { + /* calculate the DJB2 hash of the VAL portion of the string*/ + uint32_t hash = 5381; + ptr += 4; /* skip 'VAL:' */ + while (*ptr && (ptr[0] != ':' || ptr[1] != ':')) + hash = (hash << 5) + hash + *ptr++; + leaderboard->value_djb2 = hash; + } + + lboard_size = rc_lboard_size(memaddr); + if (lboard_size < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", lboard_size, read->id); + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + } + else { + /* populate the item, using the communal memrefs pool */ + rc_init_parse_state(&parse, rc_buf_reserve(buffer, lboard_size), NULL, 0); + parse.first_memref = &load_state->game->runtime.memrefs; + parse.variables = &load_state->game->runtime.variables; + leaderboard->lboard = RC_ALLOC(rc_lboard_t, &parse); + rc_parse_lboard_internal(leaderboard->lboard, memaddr, &parse); + + if (parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", parse.offset, read->id); + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; + } + else { + rc_buf_consume(buffer, parse.buffer, (char*)parse.buffer + parse.offset); + leaderboard->lboard->memrefs = NULL; /* memrefs managed by runtime */ + } + + rc_destroy_parse_state(&parse); + } + + ++leaderboard; + ++read; + } while (read < stop); + + subset->leaderboards = leaderboards; +} + +static const char* rc_client_subset_extract_title(rc_client_game_info_t* game, const char* title) +{ + const char* subset_prefix = strstr(title, "[Subset - "); + if (subset_prefix) { + const char* start = subset_prefix + 10; + const char* stop = strstr(start, "]"); + const size_t len = stop - start; + char* result = (char*)rc_buf_alloc(&game->buffer, len + 1); + + memcpy(result, start, len); + result[len] = '\0'; + return result; + } + + return NULL; +} + +static void rc_client_fetch_game_data_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; + int outstanding_requests; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(load_state->client, &load_state->async_handle)) { + rc_client_t* client = load_state->client; + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted while fetching game data"); + return; + } + + result = rc_api_process_fetch_game_data_response(&fetch_game_data_response, server_response->body); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_game_data_response.response); + + outstanding_requests = rc_client_end_load_state(load_state); + + if (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 { + rc_client_subset_info_t* subset; + + subset = (rc_client_subset_info_t*)rc_buf_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t)); + memset(subset, 0, sizeof(*subset)); + subset->public.id = fetch_game_data_response.id; + subset->active = 1; + snprintf(subset->public.badge_name, sizeof(subset->public.badge_name), "%s", fetch_game_data_response.image_name); + load_state->subset = subset; + + /* kick off the start session request while we process the game data */ + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_STARTING_SESSION, 1); + if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + /* we can't unlock achievements without a session, lock spectator mode for the game */ + load_state->client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_LOCKED; + } + else { + rc_client_begin_start_session(load_state); + } + + /* process the game data */ + rc_client_copy_achievements(load_state, subset, + fetch_game_data_response.achievements, fetch_game_data_response.num_achievements); + rc_client_copy_leaderboards(load_state, subset, + fetch_game_data_response.leaderboards, fetch_game_data_response.num_leaderboards); + + if (!load_state->game->subsets) { + /* core set */ + rc_mutex_lock(&load_state->client->state.mutex); + load_state->game->public.title = rc_buf_strcpy(&load_state->game->buffer, fetch_game_data_response.title); + load_state->game->subsets = subset; + load_state->game->public.badge_name = subset->public.badge_name; + load_state->game->public.console_id = fetch_game_data_response.console_id; + rc_mutex_unlock(&load_state->client->state.mutex); + + 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); + } + } + } + else { + rc_client_subset_info_t* scan; + + /* subset - extract subset title */ + subset->public.title = rc_client_subset_extract_title(load_state->game, fetch_game_data_response.title); + if (!subset->public.title) { + const char* core_subset_title = rc_client_subset_extract_title(load_state->game, load_state->game->public.title); + if (core_subset_title) { + rc_client_subset_info_t* scan = load_state->game->subsets; + for (; scan; scan = scan->next) { + if (scan->public.title == load_state->game->public.title) { + scan->public.title = core_subset_title; + break; + } + } + } + + subset->public.title = rc_buf_strcpy(&load_state->game->buffer, fetch_game_data_response.title); + } + + /* append to subset list */ + scan = load_state->game->subsets; + while (scan->next) + scan = scan->next; + scan->next = subset; + } + + 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) + rc_client_activate_game(load_state); + } + } + + rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); +} + +static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) +{ + rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_client_t* client = load_state->client; + rc_api_request_t request; + int result; + + if (load_state->hash->game_id == 0) { + char hash[33]; + + if (rc_hash_iterate(hash, &load_state->hash_iterator)) { + /* found another hash to try */ + load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1]; + rc_client_load_game(load_state, hash, NULL); + return; + } + + if (load_state->game->media_hash && load_state->game->media_hash->next) { + /* multiple hashes were tried, create a CSV */ + struct rc_client_media_hash_t* media_hash = load_state->game->media_hash; + int count = 1; + char* ptr; + size_t size, len; + + while (media_hash->next) { + media_hash = media_hash->next; + count++; + } + + size = count * 33; + load_state->game->public.hash = ptr = (char*)rc_buf_alloc(&load_state->game->buffer, size); + for (media_hash = load_state->game->media_hash; media_hash; media_hash = media_hash->next) { + if (ptr != load_state->game->public.hash) { + *ptr++ = ','; + size--; + } + len = snprintf(ptr, size, "%s", media_hash->game_hash->hash); + ptr += len; + size -= len; + } + } else { + /* only a single hash was tried, capture it */ + load_state->game->public.console_id = load_state->hash_console_id; + load_state->game->public.hash = load_state->hash->hash; + } + + load_state->game->public.title = "Unknown Game"; + load_state->game->public.badge_name = ""; + client->game = load_state->game; + load_state->game = NULL; + + rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game"); + return; + } + + if (load_state->hash->hash[0] != '[') { + load_state->game->public.id = load_state->hash->game_id; + load_state->game->public.hash = load_state->hash->hash; + } + + /* done with the hashing code, release the global pointer */ + g_hash_client = NULL; + + rc_mutex_lock(&client->state.mutex); + result = client->state.user; + if (result == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) + load_state->progress = RC_CLIENT_LOAD_STATE_AWAIT_LOGIN; + rc_mutex_unlock(&client->state.mutex); + + switch (result) { + case RC_CLIENT_USER_STATE_LOGGED_IN: + break; + + case RC_CLIENT_USER_STATE_LOGIN_REQUESTED: + /* do nothing, this function will be called again after login completes */ + return; + + default: + rc_client_load_error(load_state, RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED)); + 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; + fetch_game_data_request.game_id = load_state->hash->game_id; + + result = rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request); + 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_STATE_FETCHING_GAME_DATA, 1); + + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Fetching data for game %u", fetch_game_data_request.game_id); + client->callbacks.server_call(&request, rc_client_fetch_game_data_callback, load_state, client); + rc_api_destroy_request(&request); +} + +static void rc_client_identify_game_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_client_t* client = load_state->client; + rc_api_resolve_hash_response_t resolve_hash_response; + int outstanding_requests; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(client, &load_state->async_handle)) { + rc_client_load_aborted(load_state); + RC_CLIENT_LOG_VERBOSE(client, "Load aborted during game identification"); + return; + } + + result = rc_api_process_resolve_hash_response(&resolve_hash_response, server_response->body); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response); + + if (error_message) { + rc_client_end_load_state(load_state); + rc_client_load_error(load_state, result, error_message); + } + else { + /* hash exists outside the load state - always update it */ + load_state->hash->game_id = resolve_hash_response.game_id; + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + + /* have to call end_load_state after updating hash in case the load_state gets free'd */ + outstanding_requests = rc_client_end_load_state(load_state); + if (outstanding_requests < 0) { + /* previous load state was aborted, load_state was free'd */ + } + else { + rc_client_begin_fetch_game_data(load_state); + } + } + + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +static rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash) +{ + rc_client_game_hash_t* game_hash; + + rc_mutex_lock(&client->state.mutex); + game_hash = client->hashes; + while (game_hash) { + if (strcasecmp(game_hash->hash, hash) == 0) + break; + + game_hash = game_hash->next; + } + + if (!game_hash) { + game_hash = rc_buf_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; + game_hash->next = client->hashes; + client->hashes = game_hash; + } + rc_mutex_unlock(&client->state.mutex); + + return game_hash; +} + +static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, + const char* hash, const char* file_path) +{ + rc_client_t* client = load_state->client; + rc_client_game_hash_t* old_hash; + + if (client->state.load == NULL) { + rc_client_unload_game(client); + client->state.load = load_state; + + if (load_state->game == NULL) { + load_state->game = (rc_client_game_info_t*)calloc(1, sizeof(*load_state->game)); + if (!load_state->game) { + if (load_state->callback) + load_state->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + return NULL; + } + + rc_buf_init(&load_state->game->buffer); + rc_runtime_init(&load_state->game->runtime); + } + } + else if (client->state.load != load_state) { + /* previous load was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + + rc_client_free_load_state(load_state); + return NULL; + } + + old_hash = load_state->hash; + load_state->hash = rc_client_find_game_hash(client, hash); + + if (file_path) { + rc_client_media_hash_t* media_hash = + (rc_client_media_hash_t*)rc_buf_alloc(&load_state->game->buffer, sizeof(*media_hash)); + media_hash->game_hash = load_state->hash; + media_hash->path_djb2 = rc_djb2(file_path); + media_hash->next = load_state->game->media_hash; + load_state->game->media_hash = media_hash; + } + else if (load_state->game->media_hash && load_state->game->media_hash->game_hash == old_hash) { + load_state->game->media_hash->game_hash = load_state->hash; + } + + if (load_state->hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) { + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + int result; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = hash; + + result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request); + if (result != RC_OK) { + rc_client_load_error(load_state, result, rc_error_str(result)); + return NULL; + } + + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_IDENTIFYING_GAME, 1); + + client->callbacks.server_call(&request, rc_client_identify_game_callback, load_state, client); + + rc_api_destroy_request(&request); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + + rc_client_begin_fetch_game_data(load_state); + } + + return &load_state->async_handle; +} + +rc_client_async_handle_t* rc_client_begin_load_game(rc_client_t* client, const char* hash, rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_load_state_t* load_state; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!hash || !hash[0]) { + callback(RC_INVALID_STATE, "hash is required", client, callback_userdata); + return NULL; + } + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + + return rc_client_load_game(load_state, hash, NULL); +} + +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, + rc_client_callback_t callback, void* callback_userdata) +{ + rc_client_load_state_t* load_state; + char hash[33]; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (data) { + if (file_path) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p (%s)", data_size, data, file_path); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p", data_size, data); + } + } + else if (file_path) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %s", file_path); + } + else { + callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata); + 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 = "?"; + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + rc_hash_initialize_iterator(&load_state->hash_iterator, file_path, data, data_size); + + 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; + } + + load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1]; + } + else { + 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; + } + } + } + + return rc_client_load_game(load_state, hash, file_path); +} + +void rc_client_unload_game(rc_client_t* client) +{ + rc_client_game_info_t* game; + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + if (!client) + return; + + rc_mutex_lock(&client->state.mutex); + + game = client->game; + client->game = NULL; + client->state.load = NULL; + + if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) + client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_ON; + + last = &client->state.scheduled_callbacks; + do { + next = *last; + if (!next) + break; + + /* remove rich presence ping scheduled event for game */ + if (next->callback == rc_client_ping && game && next->related_id == game->public.id) { + *last = next->next; + continue; + } + + last = &next->next; + } while (1); + + rc_mutex_unlock(&client->state.mutex); + + if (game != NULL) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unloading game %u", game->public.id); + rc_client_free_game(game); + } +} + +static void rc_client_change_media(rc_client_t* client, const rc_client_game_hash_t* game_hash, rc_client_callback_t callback, void* callback_userdata) +{ + if (game_hash->game_id == client->game->public.id) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to valid media for game %u: %s", game_hash->game_id, game_hash->hash); + } + else if (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) { + RC_CLIENT_LOG_INFO(client, "Switching to unknown media"); + } + else if (game_hash->game_id == 0) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to unrecognized media: %s", game_hash->hash); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to known media for game %u: %s", game_hash->game_id, game_hash->hash); + } + + client->game->public.hash = game_hash->hash; + callback(RC_OK, NULL, client, callback_userdata); +} + +static void rc_client_identify_changed_media_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_client_t* client = load_state->client; + rc_api_resolve_hash_response_t resolve_hash_response; + + int result = rc_api_process_resolve_hash_response(&resolve_hash_response, server_response->body); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response); + + if (rc_client_async_handle_aborted(client, &load_state->async_handle)) { + RC_CLIENT_LOG_VERBOSE(client, "Media change aborted"); + /* if lookup succeeded, still capture the new hash */ + if (result == RC_OK) + load_state->hash->game_id = resolve_hash_response.game_id; + } + else if (client->game != load_state->game) { + /* loaded game changed. return success regardless of result */ + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else if (error_message) { + load_state->callback(result, error_message, client, load_state->callback_userdata); + } + else { + load_state->hash->game_id = resolve_hash_response.game_id; + + if (resolve_hash_response.game_id == 0 && client->state.hardcore) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling hardcore for unidentified media: %s", load_state->hash->hash); + rc_client_set_hardcore_enabled(client, 0); + client->game->public.hash = load_state->hash->hash; /* do still update the loaded hash */ + load_state->callback(RC_HARDCORE_DISABLED, "Hardcore disabled. Unidentified media inserted.", client, load_state->callback_userdata); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); + rc_client_change_media(client, load_state->hash, load_state->callback, load_state->callback_userdata); + } + } + + free(load_state); + rc_api_destroy_resolve_hash_response(&resolve_hash_response); +} + +rc_client_async_handle_t* rc_client_begin_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_game_hash_t* game_hash = NULL; + rc_client_media_hash_t* media_hash; + rc_client_game_info_t* game; + rc_client_pending_media_t* pending_media = NULL; + uint32_t path_djb2; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return NULL; + } + + if (!data && !file_path) { + callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata); + return NULL; + } + + rc_mutex_lock(&client->state.mutex); + if (client->state.load) { + game = client->state.load->game; + if (game->public.console_id == 0) { + /* still waiting for game data */ + pending_media = client->state.load->pending_media; + if (pending_media) { + if (pending_media->data) + free(pending_media->data); + free((void*)pending_media->file_path); + free(pending_media); + } + + pending_media = (rc_client_pending_media_t*)calloc(1, sizeof(*pending_media)); + if (!pending_media) { + rc_mutex_unlock(&client->state.mutex); + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + pending_media->file_path = strdup(file_path); + pending_media->callback = callback; + pending_media->callback_userdata = callback_userdata; + if (data && data_size) { + pending_media->data_size = data_size; + pending_media->data = (uint8_t*)malloc(data_size); + if (!pending_media->data) { + rc_mutex_unlock(&client->state.mutex); + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + memcpy(pending_media->data, data, data_size); + } + + client->state.load->pending_media = pending_media; + } + } + else { + game = client->game; + } + rc_mutex_unlock(&client->state.mutex); + + if (!game) { + callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); + return NULL; + } + + /* still waiting for game data */ + if (pending_media) + return NULL; + + /* check to see if we've already hashed this file */ + path_djb2 = rc_djb2(file_path); + rc_mutex_lock(&client->state.mutex); + for (media_hash = game->media_hash; media_hash; media_hash = media_hash->next) { + if (media_hash->path_djb2 == path_djb2) { + game_hash = media_hash->game_hash; + break; + } + } + rc_mutex_unlock(&client->state.mutex); + + if (!game_hash) { + 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 + * from other systems for games that leverage user-provided discs. */ + strcpy_s(hash, sizeof(hash), "[NO HASH]"); + } + + game_hash = rc_client_find_game_hash(client, hash); + + media_hash = (rc_client_media_hash_t*)rc_buf_alloc(&game->buffer, sizeof(*media_hash)); + media_hash->game_hash = game_hash; + media_hash->path_djb2 = path_djb2; + + rc_mutex_lock(&client->state.mutex); + media_hash->next = game->media_hash; + game->media_hash = media_hash; + rc_mutex_unlock(&client->state.mutex); + + if (!result) { + rc_client_change_media(client, game_hash, callback, callback_userdata); + return NULL; + } + } + + if (game_hash->game_id != RC_CLIENT_UNKNOWN_GAME_ID) { + rc_client_change_media(client, game_hash, callback, callback_userdata); + return NULL; + } + else { + /* call the server to make sure the hash is valid for the loaded game */ + rc_client_load_state_t* callback_data; + rc_api_resolve_hash_request_t resolve_hash_request; + rc_api_request_t request; + int result; + + memset(&resolve_hash_request, 0, sizeof(resolve_hash_request)); + resolve_hash_request.game_hash = game_hash->hash; + + result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request); + if (result != RC_OK) { + callback(result, rc_error_str(result), client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_load_state_t*)calloc(1, sizeof(rc_client_load_state_t)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return NULL; + } + + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->client = client; + callback_data->hash = game_hash; + callback_data->game = game; + + client->callbacks.server_call(&request, rc_client_identify_changed_media_callback, callback_data, client); + + rc_api_destroy_request(&request); + + return &callback_data->async_handle; + } +} + +const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client) +{ + return (client && client->game) ? &client->game->public : NULL; +} + +int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size) +{ + if (!game) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_GAME, game->badge_name); +} + +/* ===== Subsets ===== */ + +void rc_client_begin_load_subset(rc_client_t* client, uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata) +{ + char buffer[32]; + rc_client_load_state_t* load_state; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", client, callback_userdata); + return; + } + + if (!client->game) { + callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); + return; + } + + snprintf(buffer, sizeof(buffer), "[SUBSET%u]", subset_id); + + load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); + if (!load_state) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); + return; + } + + load_state->client = client; + load_state->callback = callback; + load_state->callback_userdata = callback_userdata; + load_state->game = client->game; + load_state->hash = rc_client_find_game_hash(client, buffer); + load_state->hash->game_id = subset_id; + client->state.load = load_state; + + rc_client_begin_fetch_game_data(load_state); +} + +const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->public.id == subset_id) + return &subset->public; + } + + return NULL; +} + +/* ===== Achievements ===== */ + +static void rc_client_update_achievement_display_information(rc_client_t* client, rc_client_achievement_info_t* achievement, time_t recent_unlock_time) +{ + uint8_t new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN; + uint32_t new_measured_value = 0; + + if (achievement->public.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) + return; + + achievement->public.measured_progress[0] = '\0'; + + if (achievement->public.state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) { + /* achievement unlocked */ + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + } + else { + /* active achievement */ + new_bucket = (achievement->public.category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL) ? + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + + if (achievement->trigger) { + if (achievement->trigger->measured_target) { + if (achievement->trigger->measured_value == RC_MEASURED_UNKNOWN) { + /* value hasn't been initialized yet, leave progress string empty */ + } + else if (achievement->trigger->measured_value == 0) { + /* value is 0, leave progress string empty. update progress to 0.0 */ + achievement->public.measured_percent = 0.0; + } + else { + /* clamp measured value at target (can't get more than 100%) */ + new_measured_value = (achievement->trigger->measured_value > achievement->trigger->measured_target) ? + achievement->trigger->measured_target : achievement->trigger->measured_value; + + achievement->public.measured_percent = ((float)new_measured_value * 100) / (float)achievement->trigger->measured_target; + + if (!achievement->trigger->measured_as_percent) { + snprintf(achievement->public.measured_progress, sizeof(achievement->public.measured_progress), + "%u/%u", new_measured_value, achievement->trigger->measured_target); + } + else if (achievement->public.measured_percent >= 1.0) { + snprintf(achievement->public.measured_progress, sizeof(achievement->public.measured_progress), + "%u%%", (uint32_t)achievement->public.measured_percent); + } + } + } + + if (achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE; + else if (achievement->public.measured_percent >= 80.0) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE; + } + } + + if (new_bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED && achievement->public.unlock_time >= recent_unlock_time) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED; + + achievement->public.bucket = new_bucket; +} + +static const char* rc_client_get_achievement_bucket_label(uint8_t bucket_type) +{ + switch (bucket_type) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: return "Locked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: return "Unlocked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: return "Unsupported"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: return "Unofficial"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: return "Recently Unlocked"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: return "Active Challenges"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: return "Almost There"; + default: return "Unknown"; + } +} + +static const char* rc_client_get_subset_achievement_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + const char** ptr; + const char* label; + char* new_label; + size_t new_label_len; + + switch (bucket_type) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: ptr = &subset->locked_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: ptr = &subset->unlocked_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: ptr = &subset->unofficial_label; break; + default: return rc_client_get_achievement_bucket_label(bucket_type); + } + + if (*ptr) + return *ptr; + + label = rc_client_get_achievement_bucket_label(bucket_type); + new_label_len = strlen(subset->public.title) + strlen(label) + 4; + new_label = (char*)rc_buf_alloc(&game->buffer, new_label_len); + snprintf(new_label, new_label_len, "%s - %s", subset->public.title, label); + + *ptr = new_label; + return new_label; +} + +static int rc_client_compare_achievement_unlock_times(const void* a, const void* b) +{ + const rc_client_achievement_t* unlock_a = *(const rc_client_achievement_t**)a; + const rc_client_achievement_t* unlock_b = *(const rc_client_achievement_t**)b; + return (int)(unlock_b->unlock_time - unlock_a->unlock_time); +} + +static uint8_t rc_client_map_bucket(uint8_t bucket, int grouping) +{ + if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE) { + switch (bucket) { + case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: + return RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + + case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: + case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: + return RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED; + + default: + return bucket; + } + } + + return bucket; +} + +rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* stop; + rc_client_achievement_t** bucket_achievements; + rc_client_achievement_t** achievement_ptr; + rc_client_achievement_bucket_t* bucket_ptr; + rc_client_achievement_list_t* list; + rc_client_subset_info_t* subset; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t bucket_counts[16]; + uint32_t num_buckets; + uint32_t num_achievements; + size_t buckets_size; + uint8_t bucket_type; + uint32_t num_subsets = 0; + uint32_t i, j; + const uint8_t shared_bucket_order[] = { + RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE, + RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE + }; + const uint8_t subset_bucket_order[] = { + RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED + }; + const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + + if (!client || !client->game) + return calloc(1, sizeof(rc_client_achievement_list_t)); + + memset(&bucket_counts, 0, sizeof(bucket_counts)); + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + num_subsets++; + achievement = subset->achievements; + stop = achievement + subset->public.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public.category & category) { + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + bucket_counts[achievement->public.bucket]++; + } + } + } + + num_buckets = 0; + num_achievements = 0; + for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) { + if (bucket_counts[i]) { + int needs_split = 0; + + num_achievements += bucket_counts[i]; + + if (num_subsets > 1) { + for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) { + if (subset_bucket_order[j] == i) { + needs_split = 1; + break; + } + } + } + + if (!needs_split) { + ++num_buckets; + continue; + } + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public.category & category) { + if (achievement->public.bucket == i) { + ++num_buckets; + break; + } + } + } + } + } + } + + buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_achievement_bucket_t)); + + list = (rc_client_achievement_list_t*)malloc(list_size + buckets_size + num_achievements * sizeof(rc_client_achievement_t*)); + bucket_ptr = list->buckets = (rc_client_achievement_bucket_t*)((uint8_t*)list + list_size); + achievement_ptr = (rc_client_achievement_t**)((uint8_t*)bucket_ptr + buckets_size); + + if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS) { + for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) { + bucket_type = shared_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_achievements = achievement_ptr; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + achievement = subset->achievements; + stop = achievement + subset->public.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public.bucket == bucket_type && achievement->public.category & category) + *achievement_ptr++ = &achievement->public; + } + } + + if (achievement_ptr > bucket_achievements) { + bucket_ptr->achievements = bucket_achievements; + bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements); + bucket_ptr->subset_id = 0; + bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type); + 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); + + ++bucket_ptr; + } + } + } + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { + bucket_type = subset_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_achievements = achievement_ptr; + + achievement = subset->achievements; + stop = achievement + subset->public.num_achievements; + for (; achievement < stop; ++achievement) { + if (achievement->public.category & category && + rc_client_map_bucket(achievement->public.bucket, grouping) == bucket_type) { + *achievement_ptr++ = &achievement->public; + } + } + + if (achievement_ptr > bucket_achievements) { + bucket_ptr->achievements = bucket_achievements; + bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements); + bucket_ptr->subset_id = (num_subsets > 1) ? subset->public.id : 0; + bucket_ptr->bucket_type = bucket_type; + + if (num_subsets > 1) + bucket_ptr->label = rc_client_get_subset_achievement_bucket_label(bucket_type, client->game, subset); + else + bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type); + + ++bucket_ptr; + } + } + } + + rc_mutex_unlock(&client->state.mutex); + + list->num_buckets = (uint32_t)(bucket_ptr - list->buckets); + return list; +} + +void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list) +{ + if (list) + free(list); +} + +static const rc_client_achievement_t* rc_client_subset_get_achievement_info( + rc_client_t* client, rc_client_subset_info_t* subset, uint32_t id) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public.num_achievements; + + for (; achievement < stop; ++achievement) { + if (achievement->public.id == id) { + const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + rc_mutex_lock((rc_mutex_t*)(&client->state.mutex)); + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + rc_mutex_unlock((rc_mutex_t*)(&client->state.mutex)); + return &achievement->public; + } + } + + return NULL; +} + +const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* client, uint32_t id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + const rc_client_achievement_t* achievement = rc_client_subset_get_achievement_info(client, subset, id); + if (achievement != NULL) + return achievement; + } + + return NULL; +} + +int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size) +{ + const int image_type = (state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) ? + RC_IMAGE_TYPE_ACHIEVEMENT : RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED; + + if (!achievement || !achievement->badge_name[0]) + return rc_client_get_image_url(buffer, buffer_size, image_type, "00000"); + + return rc_client_get_image_url(buffer, buffer_size, image_type, achievement->badge_name); +} + +typedef struct rc_client_award_achievement_callback_data_t +{ + uint32_t id; + uint32_t retry_count; + uint8_t hardcore; + const char* game_hash; + time_t unlock_time; + rc_client_t* client; + rc_client_scheduled_callback_data_t* scheduled_callback_data; +} rc_client_award_achievement_callback_data_t; + +static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data); + +static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, time_t now) +{ + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)callback_data->data; + + rc_client_award_achievement_server_call(ach_data); +} + +static void rc_client_award_achievement_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)callback_data; + rc_api_award_achievement_response_t award_achievement_response; + + int result = rc_api_process_award_achievement_response(&award_achievement_response, server_response->body); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &award_achievement_response.response); + + if (error_message) { + if (award_achievement_response.response.error_message) { + /* actual error from server */ + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s", ach_data->id, error_message); + rc_client_raise_server_error_event(ach_data->client, "award_achievement", award_achievement_response.response.error_message); + } + else if (ach_data->retry_count++ == 0) { + /* first retry is immediate */ + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying immediately", ach_data->id, error_message); + rc_client_award_achievement_server_call(ach_data); + return; + } + else { + /* double wait time between each attempt until we hit a maximum delay of two minutes */ + /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/ + const uint32_t delay = (ach_data->retry_count > 7) ? 120 : (1 << (ach_data->retry_count - 1)); + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying in %u seconds", ach_data->id, error_message, delay); + + if (!ach_data->scheduled_callback_data) { + ach_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*ach_data->scheduled_callback_data)); + if (!ach_data->scheduled_callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Failed to allocate scheduled callback data for reattempt to unlock achievement %u", ach_data->id); + rc_client_raise_server_error_event(ach_data->client, "award_achievement", rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + ach_data->scheduled_callback_data->callback = rc_client_award_achievement_retry; + ach_data->scheduled_callback_data->data = ach_data; + ach_data->scheduled_callback_data->related_id = ach_data->id; + } + + ach_data->scheduled_callback_data->when = time(NULL) + delay; + + rc_client_schedule_callback(ach_data->client, ach_data->scheduled_callback_data); + return; + } + } + else { + ach_data->client->user.score = award_achievement_response.new_player_score; + ach_data->client->user.score_softcore = award_achievement_response.new_player_score_softcore; + + if (award_achievement_response.awarded_achievement_id != ach_data->id) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Awarded achievement %u instead of %u", award_achievement_response.awarded_achievement_id, error_message); + } + else { + if (award_achievement_response.response.error_message) { + /* previously unlocked achievements are returned as a success with an error message */ + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u: %s", ach_data->id, award_achievement_response.response.error_message); + } + else if (ach_data->retry_count) { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded after %u attempts, new score: %u", + ach_data->id, ach_data->retry_count + 1, + ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore); + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded, new score: %u", + ach_data->id, + ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore); + } + + if (award_achievement_response.achievements_remaining == 0) { + rc_client_subset_info_t* subset; + for (subset = ach_data->client->game->subsets; subset; subset = subset->next) { + if (subset->mastery == RC_CLIENT_MASTERY_STATE_NONE && + rc_client_subset_get_achievement_info(ach_data->client, subset, ach_data->id)) { + if (subset->public.id == ach_data->client->game->public.id) { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Game %u %s", ach_data->client->game->public.id, + ach_data->client->state.hardcore ? "mastered" : "completed"); + subset->mastery = RC_CLIENT_MASTERY_STATE_PENDING; + } + else { + RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Subset %u %s", ach_data->client->game->public.id, + ach_data->client->state.hardcore ? "mastered" : "completed"); + + /* TODO: subset mastery notification */ + subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN; + } + } + } + } + } + } + + if (ach_data->scheduled_callback_data) + free(ach_data->scheduled_callback_data); + free(ach_data); +} + +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; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = ach_data->client->user.username; + api_params.api_token = ach_data->client->user.token; + api_params.achievement_id = ach_data->id; + api_params.hardcore = ach_data->hardcore; + api_params.game_hash = ach_data->game_hash; + + result = rc_api_init_award_achievement_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error constructing unlock request for achievement %u: %s", ach_data->id, rc_error_str(result)); + free(ach_data); + return; + } + + ach_data->client->callbacks.server_call(&request, rc_client_award_achievement_callback, ach_data, ach_data->client); + + rc_api_destroy_request(&request); +} + +static void rc_client_award_achievement(rc_client_t* client, rc_client_achievement_info_t* achievement) +{ + rc_client_award_achievement_callback_data_t* callback_data; + + rc_mutex_lock(&client->state.mutex); + + if (client->state.hardcore) { + achievement->public.unlock_time = achievement->unlock_time_hardcore = time(NULL); + if (achievement->unlock_time_softcore == 0) + achievement->unlock_time_softcore = achievement->unlock_time_hardcore; + + /* adjust score now - will get accurate score back from server */ + client->user.score += achievement->public.points; + } + else { + achievement->public.unlock_time = achievement->unlock_time_softcore = time(NULL); + + /* adjust score now - will get accurate score back from server */ + client->user.score_softcore += achievement->public.points; + } + + achievement->public.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + achievement->public.unlocked |= (client->state.hardcore) ? + RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE; + + rc_mutex_unlock(&client->state.mutex); + + /* can't unlock unofficial achievements on the server */ + if (achievement->public.category != RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unlocked unofficial achievement %u: %s", achievement->public.id, achievement->public.title); + return; + } + + /* don't actually unlock achievements when spectating */ + if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated achievement %u: %s", achievement->public.id, achievement->public.title); + return; + } + + callback_data = (rc_client_award_achievement_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for unlocking achievement %u", achievement->public.id); + rc_client_raise_server_error_event(client, "award_achievement", rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + callback_data->client = client; + callback_data->id = achievement->public.id; + callback_data->hardcore = client->state.hardcore; + callback_data->game_hash = client->game->public.hash; + callback_data->unlock_time = achievement->public.unlock_time; + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Awarding achievement %u: %s", achievement->public.id, achievement->public.title); + rc_client_award_achievement_server_call(callback_data); +} + +static void rc_client_subset_reset_achievements(rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public.num_achievements; + + for (; achievement < stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (!trigger || achievement->public.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + if (trigger->state == RC_TRIGGER_STATE_PRIMED) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + + rc_reset_trigger(trigger); + } +} + +static void rc_client_reset_achievements(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_reset_achievements(subset); +} + +/* ===== Leaderboards ===== */ + +static const rc_client_leaderboard_t* rc_client_subset_get_leaderboard_info(const rc_client_subset_info_t* subset, uint32_t id) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->public.id == id) + return &leaderboard->public; + } + + return NULL; +} + +const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id) +{ + rc_client_subset_info_t* subset; + + if (!client || !client->game) + return NULL; + + for (subset = client->game->subsets; subset; subset = subset->next) { + const rc_client_leaderboard_t* leaderboard = rc_client_subset_get_leaderboard_info(subset, id); + if (leaderboard != NULL) + return leaderboard; + } + + return NULL; +} + +static const char* rc_client_get_leaderboard_bucket_label(uint8_t bucket_type) +{ + switch (bucket_type) { + case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: return "Inactive"; + case RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE: return "Active"; + case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: return "Unsupported"; + case RC_CLIENT_LEADERBOARD_BUCKET_ALL: return "All"; + default: return "Unknown"; + } +} + +static const char* rc_client_get_subset_leaderboard_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + const char** ptr; + const char* label; + char* new_label; + size_t new_label_len; + + switch (bucket_type) { + case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: ptr = &subset->inactive_label; break; + case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break; + case RC_CLIENT_LEADERBOARD_BUCKET_ALL: ptr = &subset->all_label; break; + default: return rc_client_get_achievement_bucket_label(bucket_type); + } + + if (*ptr) + return *ptr; + + label = rc_client_get_leaderboard_bucket_label(bucket_type); + new_label_len = strlen(subset->public.title) + strlen(label) + 4; + new_label = (char*)rc_buf_alloc(&game->buffer, new_label_len); + snprintf(new_label, new_label_len, "%s - %s", subset->public.title, label); + + *ptr = new_label; + return new_label; +} + +static uint8_t rc_client_get_leaderboard_bucket(const rc_client_leaderboard_info_t* leaderboard, int grouping) +{ + switch (leaderboard->public.state) { + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ? + RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE; + + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + return RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED; + + default: + return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ? + RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE; + } +} + +rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping) +{ + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* stop; + rc_client_leaderboard_t** bucket_leaderboards; + rc_client_leaderboard_t** leaderboard_ptr; + rc_client_leaderboard_bucket_t* bucket_ptr; + rc_client_leaderboard_list_t* list; + rc_client_subset_info_t* subset; + const uint32_t list_size = RC_ALIGN(sizeof(*list)); + uint32_t bucket_counts[8]; + uint32_t num_buckets; + uint32_t num_leaderboards; + size_t buckets_size; + uint8_t bucket_type; + uint32_t num_subsets = 0; + uint32_t i, j; + const uint8_t shared_bucket_order[] = { + RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE + }; + const uint8_t subset_bucket_order[] = { + RC_CLIENT_LEADERBOARD_BUCKET_ALL, + RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE, + RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED + }; + + if (!client || !client->game) + return calloc(1, sizeof(rc_client_leaderboard_list_t)); + + memset(&bucket_counts, 0, sizeof(bucket_counts)); + + rc_mutex_lock(&client->state.mutex); + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + num_subsets++; + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + leaderboard->bucket = rc_client_get_leaderboard_bucket(leaderboard, grouping); + bucket_counts[leaderboard->bucket]++; + } + } + + num_buckets = 0; + num_leaderboards = 0; + for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) { + if (bucket_counts[i]) { + int needs_split = 0; + + num_leaderboards += bucket_counts[i]; + + if (num_subsets > 1) { + for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) { + if (subset_bucket_order[j] == i) { + needs_split = 1; + break; + } + } + } + + if (!needs_split) { + ++num_buckets; + continue; + } + + subset = client->game->subsets; + for (; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == i) { + ++num_buckets; + break; + } + } + } + } + } + + buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_leaderboard_bucket_t)); + + list = (rc_client_leaderboard_list_t*)malloc(list_size + buckets_size + num_leaderboards * sizeof(rc_client_leaderboard_t*)); + bucket_ptr = list->buckets = (rc_client_leaderboard_bucket_t*)((uint8_t*)list + list_size); + leaderboard_ptr = (rc_client_leaderboard_t**)((uint8_t*)bucket_ptr + buckets_size); + + if (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING) { + for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) { + bucket_type = shared_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_leaderboards = leaderboard_ptr; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == bucket_type) + *leaderboard_ptr++ = &leaderboard->public; + } + } + + if (leaderboard_ptr > bucket_leaderboards) { + bucket_ptr->leaderboards = bucket_leaderboards; + bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards); + bucket_ptr->subset_id = 0; + bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type); + bucket_ptr->bucket_type = bucket_type; + ++bucket_ptr; + } + } + } + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (!subset->active) + continue; + + for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { + bucket_type = subset_bucket_order[i]; + if (!bucket_counts[bucket_type]) + continue; + + bucket_leaderboards = leaderboard_ptr; + + leaderboard = subset->leaderboards; + stop = leaderboard + subset->public.num_leaderboards; + for (; leaderboard < stop; ++leaderboard) { + if (leaderboard->bucket == bucket_type) + *leaderboard_ptr++ = &leaderboard->public; + } + + if (leaderboard_ptr > bucket_leaderboards) { + bucket_ptr->leaderboards = bucket_leaderboards; + bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards); + bucket_ptr->subset_id = (num_subsets > 1) ? subset->public.id : 0; + bucket_ptr->bucket_type = bucket_type; + + if (num_subsets > 1) + bucket_ptr->label = rc_client_get_subset_leaderboard_bucket_label(bucket_type, client->game, subset); + else + bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type); + + ++bucket_ptr; + } + } + } + + rc_mutex_unlock(&client->state.mutex); + + list->num_buckets = (uint32_t)(bucket_ptr - list->buckets); + return list; +} + +void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list) +{ + if (list) + free(list); +} + +static void rc_client_allocate_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker; + rc_client_leaderboard_tracker_info_t* available_tracker = NULL; + + for (tracker = game->leaderboard_trackers; tracker; tracker = tracker->next) { + if (tracker->reference_count == 0) { + if (available_tracker == NULL) + available_tracker = tracker; + + continue; + } + + if (tracker->value_djb2 != leaderboard->value_djb2 || tracker->format != leaderboard->format) + continue; + + if (tracker->raw_value != leaderboard->value) { + /* if the value comes from tracking hits, we can't assume the trackers started in the + * same frame, so we can't share the tracker */ + if (tracker->value_from_hits) + continue; + + /* value has changed. prepare an update event */ + tracker->raw_value = leaderboard->value; + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } + + /* attach to the existing tracker */ + ++tracker->reference_count; + tracker->pending_events &= ~RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE; + leaderboard->tracker = tracker; + leaderboard->public.tracker_value = tracker->public.display; + return; + } + + if (!available_tracker) { + rc_client_leaderboard_tracker_info_t** next = &game->leaderboard_trackers; + + available_tracker = (rc_client_leaderboard_tracker_info_t*)rc_buf_alloc(&game->buffer, sizeof(*available_tracker)); + memset(available_tracker, 0, sizeof(*available_tracker)); + available_tracker->public.id = 1; + + for (tracker = *next; tracker; next = &tracker->next, tracker = *next) + available_tracker->public.id++; + + *next = available_tracker; + } + + /* update the claimed tracker */ + available_tracker->reference_count = 1; + available_tracker->value_djb2 = leaderboard->value_djb2; + available_tracker->format = leaderboard->format; + available_tracker->raw_value = leaderboard->value; + available_tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW; + available_tracker->value_from_hits = rc_value_from_hits(&leaderboard->lboard->value); + leaderboard->tracker = available_tracker; + leaderboard->public.tracker_value = available_tracker->public.display; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; +} + +static void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker; + leaderboard->tracker = NULL; + + if (tracker && --tracker->reference_count == 0) { + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } +} + +static void rc_client_update_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker; + if (tracker && tracker->raw_value != leaderboard->value) { + tracker->raw_value = leaderboard->value; + tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER; + } +} + +typedef struct rc_client_submit_leaderboard_entry_callback_data_t +{ + uint32_t id; + int32_t score; + uint32_t retry_count; + const char* game_hash; + time_t submit_time; + rc_client_t* client; + rc_client_scheduled_callback_data_t* scheduled_callback_data; +} rc_client_submit_leaderboard_entry_callback_data_t; + +static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data); + +static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, time_t now) +{ + rc_client_submit_leaderboard_entry_callback_data_t* lboard_data = + (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data->data; + + rc_client_submit_leaderboard_entry_server_call(lboard_data); +} + +static void rc_client_submit_leaderboard_entry_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_submit_leaderboard_entry_callback_data_t* lboard_data = + (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data; + rc_api_submit_lboard_entry_response_t submit_lboard_entry_response; + + int result = rc_api_process_submit_lboard_entry_response(&submit_lboard_entry_response, server_response->body); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &submit_lboard_entry_response.response); + + if (error_message) { + if (submit_lboard_entry_response.response.error_message) { + /* actual error from server */ + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s", lboard_data->id, error_message); + rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", submit_lboard_entry_response.response.error_message); + } + else if (lboard_data->retry_count++ == 0) { + /* first retry is immediate */ + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying immediately", lboard_data->id, error_message); + rc_client_submit_leaderboard_entry_server_call(lboard_data); + return; + } + else { + /* double wait time between each attempt until we hit a maximum delay of two minutes */ + /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/ + const uint32_t delay = (lboard_data->retry_count > 7) ? 120 : (1 << (lboard_data->retry_count - 1)); + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying in %u seconds", lboard_data->id, error_message, delay); + + if (!lboard_data->scheduled_callback_data) { + lboard_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*lboard_data->scheduled_callback_data)); + if (!lboard_data->scheduled_callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Failed to allocate scheduled callback data for reattempt to submit entry for leaderboard %u", lboard_data->id); + rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + lboard_data->scheduled_callback_data->callback = rc_client_submit_leaderboard_entry_retry; + lboard_data->scheduled_callback_data->data = lboard_data; + lboard_data->scheduled_callback_data->related_id = lboard_data->id; + } + + lboard_data->scheduled_callback_data->when = time(NULL) + delay; + + rc_client_schedule_callback(lboard_data->client, lboard_data->scheduled_callback_data); + return; + } + } + else { + /* TODO: raise event for scoreboard (if retry_count < 2) */ + + /* not currently doing anything with the response */ + if (lboard_data->retry_count) { + RC_CLIENT_LOG_INFO_FORMATTED(lboard_data->client, "Leaderboard %u submission %d completed after %u attempts", + lboard_data->id, lboard_data->score, lboard_data->retry_count); + } + } + + if (lboard_data->scheduled_callback_data) + free(lboard_data->scheduled_callback_data); + free(lboard_data); +} + +static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data) +{ + rc_api_submit_lboard_entry_request_t api_params; + rc_api_request_t request; + int result; + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = lboard_data->client->user.username; + api_params.api_token = lboard_data->client->user.token; + api_params.leaderboard_id = lboard_data->id; + api_params.score = lboard_data->score; + api_params.game_hash = lboard_data->game_hash; + + result = rc_api_init_submit_lboard_entry_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error constructing submit leaderboard entry for leaderboard %u: %s", lboard_data->id, rc_error_str(result)); + return; + } + + lboard_data->client->callbacks.server_call(&request, rc_client_submit_leaderboard_entry_callback, lboard_data, lboard_data->client); + + rc_api_destroy_request(&request); +} + +static void rc_client_submit_leaderboard_entry(rc_client_t* client, rc_client_leaderboard_info_t* leaderboard) +{ + rc_client_submit_leaderboard_entry_callback_data_t* callback_data; + + /* don't actually submit leaderboard entries when spectating */ + if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated %s (%d) for leaderboard %u: %s", + leaderboard->public.tracker_value, leaderboard->value, leaderboard->public.id, leaderboard->public.title); + return; + } + + callback_data = (rc_client_submit_leaderboard_entry_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for submitting entry for leaderboard %u", leaderboard->public.id); + rc_client_raise_server_error_event(client, "submit_lboard_entry", rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + callback_data->client = client; + callback_data->id = leaderboard->public.id; + callback_data->score = leaderboard->value; + callback_data->game_hash = client->game->public.hash; + callback_data->submit_time = time(NULL); + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Submitting %s (%d) for leaderboard %u: %s", + leaderboard->public.tracker_value, leaderboard->value, leaderboard->public.id, leaderboard->public.title); + rc_client_submit_leaderboard_entry_server_call(callback_data); +} + +static void rc_client_subset_reset_leaderboards(rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (!lboard) + continue; + + switch (leaderboard->public.state) { + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + case RC_CLIENT_LEADERBOARD_STATE_TRACKING: + rc_client_release_leaderboard_tracker(game, leaderboard); + /* fallthrough to default */ + default: + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + rc_reset_lboard(lboard); + break; + } + } +} + +static void rc_client_reset_leaderboards(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_reset_leaderboards(client->game, subset); +} + +typedef struct rc_client_fetch_leaderboard_entries_callback_data_t { + rc_client_t* client; + rc_client_fetch_leaderboard_entries_callback_t callback; + void* callback_userdata; + uint32_t leaderboard_id; + rc_client_async_handle_t async_handle; +} rc_client_fetch_leaderboard_entries_callback_data_t; + +static void rc_client_fetch_leaderboard_entries_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_fetch_leaderboard_entries_callback_data_t* lbinfo_callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)callback_data; + rc_client_t* client = lbinfo_callback_data->client; + rc_api_fetch_leaderboard_info_response_t lbinfo_response; + const char* error_message; + int result; + + if (rc_client_async_handle_aborted(client, &lbinfo_callback_data->async_handle)) { + RC_CLIENT_LOG_VERBOSE(client, "Fetch leaderbord entries aborted"); + free(lbinfo_callback_data); + return; + } + + result = rc_api_process_fetch_leaderboard_info_response(&lbinfo_response, server_response->body); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &lbinfo_response.response); + if (error_message) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch leaderboard %u info failed: %s", lbinfo_callback_data->leaderboard_id, error_message); + lbinfo_callback_data->callback(result, error_message, NULL, client, lbinfo_callback_data->callback_userdata); + } + else { + rc_client_leaderboard_entry_list_t* list; + const size_t list_size = sizeof(*list) + sizeof(rc_client_leaderboard_entry_t) * lbinfo_response.num_entries; + size_t needed_size = list_size; + unsigned i; + + for (i = 0; i < lbinfo_response.num_entries; i++) + needed_size += strlen(lbinfo_response.entries[i].username) + 1; + + list = (rc_client_leaderboard_entry_list_t*)malloc(needed_size); + if (!list) { + lbinfo_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, lbinfo_callback_data->callback_userdata); + } + else { + rc_client_leaderboard_entry_t* entry = list->entries = (rc_client_leaderboard_entry_t*)((uint8_t*)list + sizeof(*list)); + char* user = (char*)((uint8_t*)list + list_size); + const rc_api_lboard_info_entry_t* lbentry = lbinfo_response.entries; + const rc_api_lboard_info_entry_t* stop = lbentry + lbinfo_response.num_entries; + const size_t logged_in_user_len = strlen(client->user.display_name) + 1; + list->user_index = -1; + + for (; lbentry < stop; ++lbentry, ++entry) { + const size_t len = strlen(lbentry->username) + 1; + entry->user = user; + memcpy(user, lbentry->username, len); + user += len; + + if (len == logged_in_user_len && memcmp(entry->user, client->user.display_name, len) == 0) + list->user_index = (int)(entry - list->entries); + + entry->index = lbentry->index; + entry->rank = lbentry->rank; + entry->submitted = lbentry->submitted; + + rc_format_value(entry->display, sizeof(entry->display), lbentry->score, lbinfo_response.format); + } + + list->num_entries = lbinfo_response.num_entries; + + lbinfo_callback_data->callback(RC_OK, NULL, list, client, lbinfo_callback_data->callback_userdata); + } + } + + rc_api_destroy_fetch_leaderboard_info_response(&lbinfo_response); + free(lbinfo_callback_data); +} + +static rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_info(rc_client_t* client, + const rc_api_fetch_leaderboard_info_request_t* lbinfo_request, + rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_client_fetch_leaderboard_entries_callback_data_t* callback_data; + rc_api_request_t request; + int result; + const char* error_message; + + result = rc_api_init_fetch_leaderboard_info_request(&request, lbinfo_request); + + if (result != RC_OK) { + error_message = rc_error_str(result); + callback(result, error_message, NULL, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)calloc(1, sizeof(*callback_data)); + if (!callback_data) { + callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, callback_userdata); + return NULL; + } + + callback_data->client = client; + callback_data->callback = callback; + callback_data->callback_userdata = callback_userdata; + callback_data->leaderboard_id = lbinfo_request->leaderboard_id; + + client->callbacks.server_call(&request, rc_client_fetch_leaderboard_entries_callback, callback_data, client); + rc_api_destroy_request(&request); + + return &callback_data->async_handle; +} + +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id, + uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_api_fetch_leaderboard_info_request_t lbinfo_request; + + memset(&lbinfo_request, 0, sizeof(lbinfo_request)); + lbinfo_request.leaderboard_id = leaderboard_id; + lbinfo_request.first_entry = first_entry; + lbinfo_request.count = count; + + return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata); +} + +rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id, + uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata) +{ + rc_api_fetch_leaderboard_info_request_t lbinfo_request; + + memset(&lbinfo_request, 0, sizeof(lbinfo_request)); + lbinfo_request.leaderboard_id = leaderboard_id; + lbinfo_request.username = client->user.username; + lbinfo_request.count = count; + + if (!lbinfo_request.username) { + callback(RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED), NULL, client, callback_userdata); + return NULL; + } + + return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata); +} + +void rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list) +{ + if (list) + free(list); +} + +int rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size) +{ + if (!entry) + return RC_INVALID_STATE; + + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, entry->user); +} + +/* ===== Rich Presence ===== */ + +static void rc_client_ping_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_t* client = (rc_client_t*)callback_data; + rc_api_ping_response_t response; + + int result = rc_api_process_ping_response(&response, server_response->body); + const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &response.response); + if (error_message) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Ping response error: %s", error_message); + } + + rc_api_destroy_ping_response(&response); +} + +static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, time_t now) +{ + rc_api_ping_request_t api_params; + rc_api_request_t request; + char buffer[256]; + int result; + + rc_runtime_get_richpresence(&client->game->runtime, buffer, sizeof(buffer), + client->state.legacy_peek, client, NULL); + + memset(&api_params, 0, sizeof(api_params)); + api_params.username = client->user.username; + api_params.api_token = client->user.token; + api_params.game_id = client->game->public.id; + api_params.rich_presence = buffer; + + result = rc_api_init_ping_request(&request, &api_params); + if (result != RC_OK) { + RC_CLIENT_LOG_WARN_FORMATTED(client, "Error generating ping request: %s", rc_error_str(result)); + } + else { + client->callbacks.server_call(&request, rc_client_ping_callback, client, client); + } + + callback_data->when = now + 120; + rc_client_schedule_callback(client, callback_data); +} + +size_t rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size) +{ + int result; + + if (!client || !client->game || !buffer) + return 0; + + result = rc_runtime_get_richpresence(&client->game->runtime, buffer, (unsigned)buffer_size, + client->state.legacy_peek, client, NULL); + + if (result == 0) + result = snprintf(buffer, buffer_size, "Playing %s", client->game->public.title); + + return result; +} + +/* ===== Processing ===== */ + +void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler) +{ + if (client) + client->callbacks.event_handler = handler; +} + +void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler) +{ + if (client) + client->callbacks.read_memory = handler; +} + +static void rc_client_invalidate_processing_memref(rc_client_t* client) +{ + rc_memref_t** next_memref = &client->game->runtime.memrefs; + rc_memref_t* memref; + + /* invalid memref. remove from chain so we don't have to evaluate it in the future. + * it's still there, so anything referencing it will always fetch the current value. */ + while ((memref = *next_memref) != NULL) { + if (memref == client->state.processing_memref) { + *next_memref = memref->next; + break; + } + next_memref = &memref->next; + } + + rc_client_invalidate_memref_achievements(client->game, client, client->state.processing_memref); + rc_client_invalidate_memref_leaderboards(client->game, client, client->state.processing_memref); + + client->state.processing_memref = NULL; +} + +static unsigned rc_client_peek_le(unsigned address, unsigned num_bytes, void* ud) +{ + rc_client_t* client = (rc_client_t*)ud; + unsigned value = 0; + uint32_t num_read = 0; + + if (num_bytes <= sizeof(value)) { + num_read = client->callbacks.read_memory(address, (uint8_t*)&value, num_bytes, client); + if (num_read == num_bytes) + return value; + } + + if (num_read < num_bytes) + rc_client_invalidate_processing_memref(client); + + return 0; +} + +static unsigned rc_client_peek(unsigned address, unsigned num_bytes, void* ud) +{ + rc_client_t* client = (rc_client_t*)ud; + uint8_t buffer[4]; + uint32_t num_read = 0; + + switch (num_bytes) { + case 1: + num_read = client->callbacks.read_memory(address, buffer, 1, client); + if (num_read == 1) + return buffer[0]; + break; + case 2: + num_read = client->callbacks.read_memory(address, buffer, 2, client); + if (num_read == 2) + return buffer[0] | (buffer[1] << 8); + break; + case 3: + num_read = client->callbacks.read_memory(address, buffer, 3, client); + if (num_read == 3) + return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16); + break; + case 4: + num_read = client->callbacks.read_memory(address, buffer, 4, client); + if (num_read == 4) + return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24); + break; + default: + break; + } + + if (num_read < num_bytes) + rc_client_invalidate_processing_memref(client); + + return 0; +} + +void rc_client_set_legacy_peek(rc_client_t* client, int method) +{ + if (method == RC_CLIENT_LEGACY_PEEK_AUTO) { + uint8_t buffer[4] = { 1,0,0,0 }; + method = (*((uint32_t*)buffer) == 1) ? + RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS : RC_CLIENT_LEGACY_PEEK_CONSTRUCTED; + } + + client->state.legacy_peek = (method == RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS) ? + rc_client_peek_le : rc_client_peek; +} + +int rc_client_is_processing_required(rc_client_t* client) +{ + if (!client || !client->game) + return 0; + + if (client->game->runtime.trigger_count || client->game->runtime.lboard_count) + return 1; + + return (client->game->runtime.richpresence && client->game->runtime.richpresence->richpresence); +} + +static void rc_client_update_memref_values(rc_client_t* client) +{ + rc_memref_t* memref = client->game->runtime.memrefs; + unsigned value; + int invalidated_memref = 0; + + for (; memref; memref = memref->next) { + if (memref->value.is_indirect) + continue; + + client->state.processing_memref = memref; + + value = rc_peek_value(memref->address, memref->value.size, client->state.legacy_peek, client); + + if (client->state.processing_memref) { + rc_update_memref_value(&memref->value, value); + } + else { + /* if the peek function cleared the processing_memref, the memref was invalidated */ + invalidated_memref = 1; + } + } + + client->state.processing_memref = NULL; + + if (invalidated_memref) + rc_client_update_active_achievements(client->game); +} + +static void rc_client_do_frame_process_achievements(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public.num_achievements; + float best_progress = 0.0; + rc_client_achievement_info_t* best_progress_achievement = NULL; + + for (; achievement < stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + int old_state, new_state; + unsigned old_measured_value; + + if (!trigger || achievement->public.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + old_measured_value = trigger->measured_value; + old_state = trigger->state; + new_state = rc_evaluate_trigger(trigger, client->state.legacy_peek, client, NULL); + + /* if the measured value changed and the achievement hasn't triggered, show a progress indicator */ + if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN && + trigger->measured_value <= trigger->measured_target && + rc_trigger_state_active(new_state) && new_state != RC_TRIGGER_STATE_WAITING) { + + /* only show a popup for the achievement closest to triggering */ + float progress = (float)trigger->measured_value / (float)trigger->measured_target; + + if (trigger->measured_as_percent) { + /* if reporting the measured value as a percentage, only show the popup if the percentage changes */ + const unsigned old_percent = (unsigned)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); + const unsigned new_percent = (unsigned)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); + if (old_percent == new_percent) + progress = -1.0; + } + + if (progress > best_progress) { + best_progress = progress; + + if (best_progress_achievement) + best_progress_achievement->pending_events &= ~RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_PROGRESS_INDICATOR_SHOW; + + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_PROGRESS_INDICATOR_SHOW; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + best_progress_achievement = achievement; + } + } + + /* if the state hasn't changed, there won't be any events raised */ + if (new_state == old_state) + continue; + + /* raise a CHALLENGE_INDICATOR_HIDE event when changing from PRIMED to anything else */ + if (old_state == RC_TRIGGER_STATE_PRIMED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + + /* raise events for each of the possible new states */ + if (new_state == RC_TRIGGER_STATE_TRIGGERED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED; + else if (new_state == RC_TRIGGER_STATE_PRIMED) + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW; + + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } +} + +static void rc_client_raise_achievement_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement = subset->achievements; + rc_client_achievement_info_t* stop = achievement + subset->public.num_achievements; + rc_client_event_t client_event; + time_t recent_unlock_time = 0; + + memset(&client_event, 0, sizeof(client_event)); + + for (; achievement < stop; ++achievement) { + if (achievement->pending_events == RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE) + continue; + + /* kick off award achievement request first */ + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) { + rc_client_award_achievement(client, achievement); + client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS; + } + + /* update display state */ + if (recent_unlock_time == 0) + recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS; + rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); + + /* raise events*/ + client_event.achievement = &achievement->public; + + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE; + client->callbacks.event_handler(&client_event, client); + } + else if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW; + client->callbacks.event_handler(&client_event, client); + } + + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_PROGRESS_INDICATOR_SHOW) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW; + client->callbacks.event_handler(&client_event, client); + } + + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) { + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED; + client->callbacks.event_handler(&client_event, client); + } + + /* clear pending flags */ + achievement->pending_events = RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE; + } +} + +static void rc_client_raise_mastery_event(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_GAME_COMPLETED; + + subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN; + + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_do_frame_process_leaderboards(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* stop = leaderboard + subset->public.num_leaderboards; + + for (; leaderboard < stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + int old_state, new_state; + + switch (leaderboard->public.state) { + case RC_CLIENT_LEADERBOARD_STATE_INACTIVE: + case RC_CLIENT_LEADERBOARD_STATE_DISABLED: + continue; + + default: + if (!lboard) + continue; + + break; + } + + old_state = lboard->state; + new_state = rc_evaluate_lboard(lboard, &leaderboard->value, client->state.legacy_peek, client, NULL); + + switch (new_state) { + case RC_LBOARD_STATE_STARTED: /* leaderboard is running */ + if (old_state != RC_LBOARD_STATE_STARTED) { + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED; + rc_client_allocate_leaderboard_tracker(client->game, leaderboard); + } + else { + rc_client_update_leaderboard_tracker(client->game, leaderboard); + } + break; + + case RC_LBOARD_STATE_CANCELED: + if (old_state != RC_LBOARD_STATE_CANCELED) { + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + rc_client_release_leaderboard_tracker(client->game, leaderboard); + } + break; + + case RC_LBOARD_STATE_TRIGGERED: + if (old_state != RC_RUNTIME_EVENT_LBOARD_TRIGGERED) { + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED; + rc_client_release_leaderboard_tracker(client->game, leaderboard); + } + break; + } + + if (leaderboard->pending_events) + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD; + } +} + +static void rc_client_raise_leaderboard_tracker_events(rc_client_t* client) +{ + rc_client_leaderboard_tracker_info_t* tracker = client->game->leaderboard_trackers; + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + tracker = client->game->leaderboard_trackers; + for (; tracker; tracker = tracker->next) { + if (tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE) + continue; + + client_event.leaderboard_tracker = &tracker->public; + + if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE) { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE; + client->callbacks.event_handler(&client_event, client); + } + else { + rc_format_value(tracker->public.display, sizeof(tracker->public.display), tracker->raw_value, tracker->format); + + if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW; + client->callbacks.event_handler(&client_event, client); + } + else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE) { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE; + client->callbacks.event_handler(&client_event, client); + } + } + + tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE; + } +} + +static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + rc_client_leaderboard_info_t* leaderboard = subset->leaderboards; + rc_client_leaderboard_info_t* leaderboard_stop = leaderboard + subset->public.num_leaderboards; + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + for (; leaderboard < leaderboard_stop; ++leaderboard) { + if (leaderboard->pending_events == RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE) + continue; + + client_event.leaderboard = &leaderboard->public; + + if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u canceled: %s", leaderboard->public.id, leaderboard->public.title); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_FAILED; + client->callbacks.event_handler(&client_event, client); + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED) { + /* kick off submission request before raising event */ + rc_client_submit_leaderboard_entry(client, leaderboard); + + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED; + client->callbacks.event_handler(&client_event, client); + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u started: %s", leaderboard->public.id, leaderboard->public.title); + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_STARTED; + client->callbacks.event_handler(&client_event, client); + } + + leaderboard->pending_events = RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE; + } +} + +static void rc_client_reset_pending_events(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + + client->game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE; + + for (subset = client->game->subsets; subset; subset = subset->next) + subset->pending_events = RC_CLIENT_SUBSET_PENDING_EVENT_NONE; +} + +static void rc_client_subset_raise_pending_events(rc_client_t* client, rc_client_subset_info_t* subset) +{ + /* raise any pending achievement events */ + if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT) + rc_client_raise_achievement_events(client, subset); + + /* raise any pending leaderboard events */ + if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD) + rc_client_raise_leaderboard_events(client, subset); + + /* raise mastery event if pending */ + if (subset->mastery == RC_CLIENT_MASTERY_STATE_PENDING) + rc_client_raise_mastery_event(client, subset); +} + +static void rc_client_raise_pending_events(rc_client_t* client) +{ + rc_client_subset_info_t* subset; + + /* raise tracker events before leaderboard events so formatted values are updated for leaderboard events */ + if (client->game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER) + rc_client_raise_leaderboard_tracker_events(client); + + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_raise_pending_events(client, subset); + + /* if any achievements were unlocked, resync the active achievements list */ + if (client->game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS) { + rc_mutex_lock(&client->state.mutex); + rc_client_update_active_achievements(client->game); + rc_mutex_unlock(&client->state.mutex); + } +} + +void rc_client_do_frame(rc_client_t* client) +{ + if (!client) + return; + + if (client->game && !client->game->waiting_for_reset) { + rc_runtime_richpresence_t* richpresence; + rc_client_subset_info_t* subset; + + rc_mutex_lock(&client->state.mutex); + + rc_client_reset_pending_events(client); + + rc_client_update_memref_values(client); + rc_update_variables(client->game->runtime.variables, client->state.legacy_peek, client, NULL); + + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active) + rc_client_do_frame_process_achievements(client, subset); + } + + if (client->state.hardcore) { + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active) + rc_client_do_frame_process_leaderboards(client, subset); + } + } + + richpresence = client->game->runtime.richpresence; + if (richpresence && richpresence->richpresence) + rc_update_richpresence(richpresence->richpresence, client->state.legacy_peek, client, NULL); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client); + } + + rc_client_idle(client); +} + +void rc_client_idle(rc_client_t* client) +{ + rc_client_scheduled_callback_data_t* scheduled_callback; + + if (!client) + return; + + scheduled_callback = client->state.scheduled_callbacks; + if (scheduled_callback) { + const time_t now = time(NULL); + + do { + rc_mutex_lock(&client->state.mutex); + scheduled_callback = client->state.scheduled_callbacks; + if (scheduled_callback) { + if (scheduled_callback->when > now) { + /* not time for next callback yet, ignore it */ + scheduled_callback = NULL; + } + else { + /* remove the callback from the queue while we process it. callback can requeue if desired */ + client->state.scheduled_callbacks = scheduled_callback->next; + } + } + rc_mutex_unlock(&client->state.mutex); + + if (!scheduled_callback) + break; + + scheduled_callback->callback(scheduled_callback, client, now); + } while (1); + } +} + +void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback) +{ + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + rc_mutex_lock(&client->state.mutex); + + last = &client->state.scheduled_callbacks; + do { + next = *last; + if (next == NULL || next->when > scheduled_callback->when) { + scheduled_callback->next = next; + *last = scheduled_callback; + break; + } + + last = &next->next; + } while (1); + + rc_mutex_unlock(&client->state.mutex); +} + +static void rc_client_reset_richpresence(rc_client_t* client) +{ + rc_runtime_richpresence_t* richpresence = client->game->runtime.richpresence; + if (richpresence && richpresence->richpresence) + rc_reset_richpresence(richpresence->richpresence); +} + +static void rc_client_reset_variables(rc_client_t* client) +{ + rc_value_t* variable = client->game->runtime.variables; + for (; variable; variable = variable->next) + rc_reset_value(variable); +} + +static void rc_client_reset_all(rc_client_t* client) +{ + rc_client_reset_achievements(client); + rc_client_reset_leaderboards(client); + rc_client_reset_richpresence(client); + rc_client_reset_variables(client); +} + +void rc_client_reset(rc_client_t* client) +{ + rc_client_game_hash_t* game_hash; + if (!client || !client->game) + return; + + game_hash = rc_client_find_game_hash(client, client->game->public.hash); + if (game_hash && game_hash->game_id != client->game->public.id) { + /* current media is not for loaded game. unload game */ + RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling runtime. Reset with non-game media loaded: %u (%s)", + (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) ? 0 : game_hash->game_id, game_hash->hash); + rc_client_unload_game(client); + return; + } + + RC_CLIENT_LOG_INFO(client, "Resetting runtime"); + + rc_mutex_lock(&client->state.mutex); + + client->game->waiting_for_reset = 0; + rc_client_reset_pending_events(client); + + rc_client_reset_all(client); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client); +} + +size_t rc_client_progress_size(rc_client_t* client) +{ + size_t result; + + if (!client || !client->game) + return 0; + + rc_mutex_lock(&client->state.mutex); + result = rc_runtime_progress_size(&client->game->runtime, NULL); + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +int rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer) +{ + int result; + + if (!client || !client->game) + return RC_NO_GAME_LOADED; + + if (!buffer) + return RC_INVALID_STATE; + + rc_mutex_lock(&client->state.mutex); + result = rc_runtime_serialize_progress(buffer, &client->game->runtime, NULL); + rc_mutex_unlock(&client->state.mutex); + + return result; +} + +static void rc_client_subset_before_deserialize_progress(rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + + /* flag any visible challenge indicators to be hidden */ + achievement = subset->achievements; + achievement_stop = achievement + subset->public.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (trigger && trigger->state == RC_TRIGGER_STATE_PRIMED && + achievement->public.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + + /* flag any visible trackers to be hidden */ + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (lboard && lboard->state == RC_LBOARD_STATE_STARTED && + leaderboard->public.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) { + leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } +} + +static void rc_client_subset_after_deserialize_progress(rc_client_game_info_t* game, rc_client_subset_info_t* subset) +{ + rc_client_achievement_info_t* achievement; + rc_client_achievement_info_t* achievement_stop; + rc_client_leaderboard_info_t* leaderboard; + rc_client_leaderboard_info_t* leaderboard_stop; + + /* flag any challenge indicators that should be shown */ + achievement = subset->achievements; + achievement_stop = achievement + subset->public.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + rc_trigger_t* trigger = achievement->trigger; + if (!trigger || achievement->public.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) + continue; + + if (trigger->state == RC_TRIGGER_STATE_PRIMED) { + /* if it's already shown, just keep it. otherwise flag it to be shown */ + if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) { + achievement->pending_events &= ~RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE; + } + else { + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + } + } + /* ASSERT: only active achievements are serialized, so we don't have to worry about + * deserialization deactiving them. */ + } + + /* flag any trackers that need to be shown */ + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + rc_lboard_t* lboard = leaderboard->lboard; + if (!lboard || + leaderboard->public.state == RC_CLIENT_LEADERBOARD_STATE_INACTIVE || + leaderboard->public.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED) + continue; + + if (lboard->state == RC_LBOARD_STATE_STARTED) { + leaderboard->value = (int)lboard->value.value.value; + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING; + + /* if it's already being tracked, just update tracker. otherwise, allocate one */ + if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + rc_client_update_leaderboard_tracker(game, leaderboard); + } + else { + rc_client_allocate_leaderboard_tracker(game, leaderboard); + } + } + else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) { + /* deallocate the tracker (don't actually raise the failed event) */ + leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED; + leaderboard->public.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + rc_client_release_leaderboard_tracker(game, leaderboard); + } + } +} + +int rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized) +{ + rc_client_subset_info_t* subset; + int result; + + if (!client || !client->game) + return RC_NO_GAME_LOADED; + + rc_mutex_lock(&client->state.mutex); + + rc_client_reset_pending_events(client); + + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_before_deserialize_progress(subset); + + if (!serialized) { + rc_client_reset_all(client); + result = RC_OK; + } + else { + result = rc_runtime_deserialize_progress(&client->game->runtime, serialized, NULL); + } + + for (subset = client->game->subsets; subset; subset = subset->next) + rc_client_subset_after_deserialize_progress(client->game, subset); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client); + + return result; +} + +/* ===== Toggles ===== */ + +static void rc_client_enable_hardcore(rc_client_t* client) +{ + client->state.hardcore = 1; + + if (client->game) { + rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE); + rc_client_activate_leaderboards(client->game, client); + + /* disable processing until the client acknowledges the reset event by calling rc_runtime_reset() */ + RC_CLIENT_LOG_INFO(client, "Hardcore enabled, waiting for reset"); + client->game->waiting_for_reset = 1; + } + else { + RC_CLIENT_LOG_INFO(client, "Hardcore enabled"); + } +} + +static void rc_client_disable_hardcore(rc_client_t* client) +{ + client->state.hardcore = 0; + RC_CLIENT_LOG_INFO(client, "Hardcore disabled"); + + if (client->game) { + rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + rc_client_deactivate_leaderboards(client->game, client); + } +} + +void rc_client_set_hardcore_enabled(rc_client_t* client, int enabled) +{ + int changed = 0; + + if (!client) + return; + + rc_mutex_lock(&client->state.mutex); + + enabled = enabled ? 1 : 0; + if (client->state.hardcore != enabled) { + if (enabled) + rc_client_enable_hardcore(client); + else + rc_client_disable_hardcore(client); + + changed = 1; + } + + rc_mutex_unlock(&client->state.mutex); + + /* events must be raised outside of lock */ + if (changed && client->game) { + if (enabled) { + /* if enabling hardcore, notify client that a reset is requested */ + if (client->game->waiting_for_reset) { + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + client_event.type = RC_CLIENT_EVENT_RESET; + client->callbacks.event_handler(&client_event, client); + } + } + else { + /* if disabling hardcore, leaderboards will be deactivated. raise events for hiding trackers */ + rc_client_raise_pending_events(client); + } + } +} + +int rc_client_get_hardcore_enabled(const rc_client_t* client) +{ + return client && client->state.hardcore; +} + +void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled) +{ + if (client) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Unofficial %s", enabled ? "enabled" : "disabled"); + client->state.unofficial_enabled = enabled ? 1 : 0; + } +} + +int rc_client_get_unofficial_enabled(const rc_client_t* client) +{ + return client && client->state.unofficial_enabled; +} + +void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled) +{ + if (client) { + RC_CLIENT_LOG_INFO_FORMATTED(client, "Encore mode %s", enabled ? "enabled" : "disabled"); + client->state.encore_mode = enabled ? 1 : 0; + } +} + +int rc_client_get_encore_mode_enabled(const rc_client_t* client) +{ + return client && client->state.encore_mode; +} + +void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled) +{ + if (client) { + if (!enabled && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) { + RC_CLIENT_LOG_WARN(client, "Spectator mode cannot be disabled if it was enabled prior to loading game."); + return; + } + + RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectator mode %s", enabled ? "enabled" : "disabled"); + client->state.spectator_mode = enabled ? RC_CLIENT_SPECTATOR_MODE_ON : RC_CLIENT_SPECTATOR_MODE_OFF; + } +} + +int rc_client_get_spectator_mode_enabled(const rc_client_t* client) +{ + return client && (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) ? 0 : 1; +} + +void rc_client_set_userdata(rc_client_t* client, void* userdata) +{ + if (client) + client->callbacks.client_data = userdata; +} + +void* rc_client_get_userdata(const rc_client_t* client) +{ + return client ? client->callbacks.client_data : NULL; +} + +void rc_client_set_host(const rc_client_t* client, const char* hostname) +{ + /* if empty, just pass NULL */ + if (hostname && !hostname[0]) + hostname = NULL; + + /* clear the image host so it'll use the custom host for images too */ + rc_api_set_image_host(NULL); + + /* set the custom host */ + if (hostname && client) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Using host: %s", hostname); + } + rc_api_set_host(hostname); +} diff --git a/src/rcheevos/rc_client_internal.h b/src/rcheevos/rc_client_internal.h new file mode 100644 index 00000000..2c0047fa --- /dev/null +++ b/src/rcheevos/rc_client_internal.h @@ -0,0 +1,259 @@ +#ifndef RC_CLIENT_INTERNAL_H +#define RC_CLIENT_INTERNAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "rc_client.h" + +#include "rc_compat.h" +#include "rc_runtime.h" +#include "rc_runtime_types.h" + +typedef struct rc_client_callbacks_t { + rc_client_read_memory_func_t read_memory; + rc_client_event_handler_t event_handler; + rc_client_server_call_t server_call; + rc_client_message_callback_t log_call; + + void* client_data; +} rc_client_callbacks_t; + +struct rc_client_scheduled_callback_data_t; +typedef void (*rc_client_scheduled_callback_t)(struct rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, time_t now); + +typedef struct rc_client_scheduled_callback_data_t +{ + time_t when; + unsigned related_id; + rc_client_scheduled_callback_t callback; + void* data; + struct rc_client_scheduled_callback_data_t* next; +} rc_client_scheduled_callback_data_t; + +void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback); + +enum { + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE = 0, + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED = (1 << 1), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW = (1 << 2), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE = (1 << 3), + RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_PROGRESS_INDICATOR_SHOW = (1 << 4) +}; + +typedef struct rc_client_achievement_info_t { + rc_client_achievement_t public; + + rc_trigger_t* trigger; + uint8_t md5[16]; + + time_t unlock_time_hardcore; + time_t unlock_time_softcore; + + uint8_t pending_events; +} rc_client_achievement_info_t; + +enum { + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE = 0, + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE = (1 << 1), + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW = (1 << 2), + RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE = (1 << 3) +}; + +typedef struct rc_client_leaderboard_tracker_info_t { + rc_client_leaderboard_tracker_t public; + struct rc_client_leaderboard_tracker_info_t* next; + int raw_value; + + uint32_t value_djb2; + + uint8_t format; + uint8_t pending_events; + uint8_t reference_count; + uint8_t value_from_hits; +} rc_client_leaderboard_tracker_info_t; + +enum { + RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE = 0, + RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED = (1 << 1), + RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED = (1 << 2), + RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED = (1 << 3) +}; + +typedef struct rc_client_leaderboard_info_t { + rc_client_leaderboard_t public; + + rc_lboard_t* lboard; + uint8_t md5[16]; + + rc_client_leaderboard_tracker_info_t* tracker; + + uint32_t value_djb2; + int value; + + uint8_t format; + uint8_t pending_events; + uint8_t bucket; +} rc_client_leaderboard_info_t; + +enum { + RC_CLIENT_SUBSET_PENDING_EVENT_NONE = 0, + RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT = (1 << 1), + RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD = (1 << 2) +}; + +enum { + RC_CLIENT_GAME_PENDING_EVENT_NONE = 0, + RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER = (1 << 1), + RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS = (1 << 2) +}; + +typedef struct rc_client_subset_info_t { + rc_client_subset_t public; + + rc_client_achievement_info_t* achievements; + rc_client_leaderboard_info_t* leaderboards; + + struct rc_client_subset_info_t* next; + + const char* all_label; + const char* inactive_label; + const char* locked_label; + const char* unlocked_label; + const char* unofficial_label; + const char* unsupported_label; + + uint8_t active; + uint8_t mastery; + uint8_t pending_events; +} rc_client_subset_info_t; + +typedef struct rc_client_game_hash_t { + char hash[33]; + uint32_t game_id; + struct rc_client_game_hash_t* next; +} rc_client_game_hash_t; + +typedef struct rc_client_media_hash_t { + rc_client_game_hash_t* game_hash; + struct rc_client_media_hash_t* next; + uint32_t path_djb2; +} rc_client_media_hash_t; + +typedef struct rc_client_game_info_t { + rc_client_game_t public; + rc_client_leaderboard_tracker_info_t* leaderboard_trackers; + + rc_client_subset_info_t* subsets; + + rc_client_media_hash_t* media_hash; + + rc_runtime_t runtime; + uint8_t waiting_for_reset; + + uint8_t pending_events; + + rc_api_buffer_t buffer; +} rc_client_game_info_t; + +enum { + RC_CLIENT_LOAD_STATE_NONE, + RC_CLIENT_LOAD_STATE_IDENTIFYING_GAME, + RC_CLIENT_LOAD_STATE_AWAIT_LOGIN, + RC_CLIENT_LOAD_STATE_FETCHING_GAME_DATA, + RC_CLIENT_LOAD_STATE_STARTING_SESSION, + RC_CLIENT_LOAD_STATE_DONE, + RC_CLIENT_LOAD_STATE_UNKNOWN_GAME +}; + +enum { + RC_CLIENT_USER_STATE_NONE, + RC_CLIENT_USER_STATE_LOGIN_REQUESTED, + RC_CLIENT_USER_STATE_LOGGED_IN +}; + +enum { + RC_CLIENT_MASTERY_STATE_NONE, + RC_CLIENT_MASTERY_STATE_PENDING, + RC_CLIENT_MASTERY_STATE_SHOWN +}; + +enum { + RC_CLIENT_SPECTATOR_MODE_OFF, + RC_CLIENT_SPECTATOR_MODE_ON, + RC_CLIENT_SPECTATOR_MODE_LOCKED +}; + +struct rc_client_load_state_t; + +typedef struct rc_client_state_t { + rc_mutex_t mutex; + rc_api_buffer_t buffer; + + rc_client_scheduled_callback_data_t* scheduled_callbacks; + + uint8_t hardcore; + uint8_t encore_mode; + uint8_t spectator_mode; + uint8_t unofficial_enabled; + uint8_t log_level; + uint8_t user; + + struct rc_client_load_state_t* load; + rc_memref_t* processing_memref; + + rc_peek_t legacy_peek; +} rc_client_state_t; + +struct rc_client_t { + rc_client_game_info_t* game; + rc_client_game_hash_t* hashes; + + rc_client_user_t user; + + rc_client_callbacks_t callbacks; + + rc_client_state_t state; +}; + +#ifdef RC_NO_VARIADIC_MACROS + void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...); + void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...); +#else + void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...); + #define RC_CLIENT_LOG_ERR_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_WARN_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_INFO_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) rc_client_log_message_formatted(client, format, __VA_ARGS__); } + #define RC_CLIENT_LOG_VERBOSE_FORMATTED(client, format, ...) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) rc_client_log_message_formatted(client, format, __VA_ARGS__); } +#endif + +void rc_client_log_message(const rc_client_t* client, const char* message); +#define RC_CLIENT_LOG_ERR(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_WARN(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_INFO(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) rc_client_log_message(client, message); } +#define RC_CLIENT_LOG_VERBOSE(client, message) { if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) rc_client_log_message(client, message); } + +/* internals pulled from runtime.c */ +void rc_runtime_checksum(const char* memaddr, unsigned char* md5); +int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref); +int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref); +/* end runtime.c internals */ + +enum { + RC_CLIENT_LEGACY_PEEK_AUTO, + RC_CLIENT_LEGACY_PEEK_CONSTRUCTED, + RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS +}; + +void rc_client_set_legacy_peek(rc_client_t* client, int method); + +void rc_client_begin_load_subset(rc_client_t* client, uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_RUNTIME_H */ diff --git a/src/rcheevos/rc_compat.h b/src/rcheevos/rc_compat.h index f396726d..e22f9b84 100644 --- a/src/rcheevos/rc_compat.h +++ b/src/rcheevos/rc_compat.h @@ -13,6 +13,8 @@ extern "C" { /* MinGW redefinitions */ +#define RC_NO_VARIADIC_MACROS 1 + #elif defined(_MSC_VER) /* Visual Studio redefinitions */ @@ -32,6 +34,8 @@ extern "C" { /* C89 redefinitions */ #define RC_C89_HELPERS 1 +#define RC_NO_VARIADIC_MACROS 1 + #ifndef snprintf extern int rc_snprintf(char* buffer, size_t size, const char* format, ...); #define snprintf rc_snprintf @@ -65,6 +69,29 @@ extern "C" { #define gmtime_s rc_gmtime_s #endif +#ifdef RC_NO_THREADS + typedef int rc_mutex_t; + + #define rc_mutex_init(mutex) + #define rc_mutex_destroy(mutex) + #define rc_mutex_lock(mutex) + #define rc_mutex_unlock(mutex) +#else + #ifdef _WIN32 + typedef struct rc_mutex_t { + void* handle; /* HANDLE is defined as "void*" */ + } rc_mutex_t; + #else + #include + typedef pthread_mutex_t rc_mutex_t; + #endif + + void rc_mutex_init(rc_mutex_t* mutex); + void rc_mutex_destroy(rc_mutex_t* mutex); + void rc_mutex_lock(rc_mutex_t* mutex); + void rc_mutex_unlock(rc_mutex_t* mutex); +#endif + #ifdef __cplusplus } #endif diff --git a/src/rcheevos/rc_internal.h b/src/rcheevos/rc_internal.h index 7f80d179..e58ca53d 100644 --- a/src/rcheevos/rc_internal.h +++ b/src/rcheevos/rc_internal.h @@ -36,6 +36,9 @@ RC_ALLOW_ALIGN(char) #define RC_ALLOC(t, p) ((t*)rc_alloc((p)->buffer, &(p)->offset, sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) #define RC_ALLOC_SCRATCH(t, p) ((t*)rc_alloc_scratch((p)->buffer, &(p)->offset, sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) +/* force alignment to 4 bytes on 32-bit systems, or 8 bytes on 64-bit systems */ +#define RC_ALIGN(n) (((n) + (sizeof(void*)-1)) & ~(sizeof(void*)-1)) + typedef struct rc_scratch_buffer { struct rc_scratch_buffer* next; int offset; @@ -132,6 +135,8 @@ void* rc_alloc(void* pointer, int* offset, int size, int alignment, rc_scratch_t void* rc_alloc_scratch(void* pointer, int* offset, int size, int alignment, rc_scratch_t* scratch, int scratch_object_pointer_offset); char* rc_alloc_str(rc_parse_state_t* parse, const char* text, int length); +unsigned rc_djb2(const char* input); + rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, unsigned address, char size, char is_indirect); int rc_parse_memref(const char** memaddr, char* size, unsigned* address); void rc_update_memref_values(rc_memref_t* memref, rc_peek_t peek, void* ud); @@ -140,6 +145,7 @@ unsigned rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_stat char rc_memref_shared_size(char size); unsigned rc_memref_mask(char size); void rc_transform_memref_value(rc_typed_value_t* value, char size); +unsigned rc_peek_value(unsigned address, char size, rc_peek_t peek, void* ud); void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_parse_state_t* parse); int rc_trigger_state_active(int state); @@ -177,6 +183,7 @@ int rc_operand_is_float(const rc_operand_t* self); void rc_parse_value_internal(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse); int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t peek, void* ud, lua_State* L); void rc_reset_value(rc_value_t* self); +int rc_value_from_hits(rc_value_t* self); rc_value_t* rc_alloc_helper_variable(const char* memaddr, int memaddr_len, rc_parse_state_t* parse); void rc_update_variables(rc_value_t* variable, rc_peek_t peek, void* ud, lua_State* L); diff --git a/src/rcheevos/rc_libretro.c b/src/rcheevos/rc_libretro.c index 541f62a4..eab44ef1 100644 --- a/src/rcheevos/rc_libretro.c +++ b/src/rcheevos/rc_libretro.c @@ -316,6 +316,31 @@ unsigned char* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regio return rc_libretro_memory_find_avail(regions, address, NULL); } +uint32_t rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, unsigned address, + uint8_t* buffer, uint32_t num_bytes) { + unsigned i; + uint32_t avail; + + for (i = 0; i < regions->count; ++i) { + const size_t size = regions->size[i]; + if (address < size) { + if (regions->data[i] == NULL) + break; + + avail = (unsigned)(size - address); + if (avail < num_bytes) + return avail; + + memcpy(buffer, ®ions->data[i][address], num_bytes); + return num_bytes; + } + + address -= (unsigned)size; + } + + return 0; +} + void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback) { rc_libretro_verbose_message_callback = callback; } @@ -650,7 +675,7 @@ void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, file_len = rc_file_tell(file_handle); rc_file_seek(file_handle, 0, SEEK_SET); - m3u_contents = (char*)malloc(file_len + 1); + m3u_contents = (char*)malloc((size_t)file_len + 1); rc_file_read(file_handle, m3u_contents, (int)file_len); m3u_contents[file_len] = '\0'; diff --git a/src/rcheevos/rc_libretro.h b/src/rcheevos/rc_libretro.h index 642e3cbd..089631ba 100644 --- a/src/rcheevos/rc_libretro.h +++ b/src/rcheevos/rc_libretro.h @@ -54,6 +54,7 @@ void rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions); unsigned char* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, unsigned address); unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, unsigned address, unsigned* avail); +uint32_t rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, unsigned address, uint8_t* buffer, uint32_t num_bytes); /*****************************************************************************\ | Disk Identification | diff --git a/src/rcheevos/richpresence.c b/src/rcheevos/richpresence.c index b7d883e8..3ad6bd99 100644 --- a/src/rcheevos/richpresence.c +++ b/src/rcheevos/richpresence.c @@ -280,7 +280,6 @@ static void rc_rebalance_richpresence_lookup(rc_richpresence_lookup_item_t** roo { rc_richpresence_lookup_item_t** items; rc_scratch_buffer_t* buffer; - const int alignment = sizeof(rc_richpresence_lookup_item_t*); int index; int size; @@ -293,7 +292,7 @@ static void rc_rebalance_richpresence_lookup(rc_richpresence_lookup_item_t** roo size = count * sizeof(rc_richpresence_lookup_item_t*); buffer = &parse->scratch.buffer; do { - const int aligned_offset = (buffer->offset + alignment - 1) & ~(alignment - 1); + const int aligned_offset = RC_ALIGN(buffer->offset); const int remaining = sizeof(buffer->buffer) - aligned_offset; if (remaining >= size) { diff --git a/src/rcheevos/runtime.c b/src/rcheevos/runtime.c index 49545c6e..a5a66abd 100644 --- a/src/rcheevos/runtime.c +++ b/src/rcheevos/runtime.c @@ -65,7 +65,7 @@ void rc_runtime_destroy(rc_runtime_t* self) { } } -static void rc_runtime_checksum(const char* memaddr, unsigned char* md5) { +void rc_runtime_checksum(const char* memaddr, unsigned char* md5) { md5_state_t state; md5_init(&state); md5_append(&state, (unsigned char*)memaddr, (int)strlen(memaddr)); @@ -727,13 +727,8 @@ void rc_runtime_reset(rc_runtime_t* self) { rc_reset_lboard(self->lboards[i].lboard); } - if (self->richpresence && self->richpresence->richpresence) { - rc_richpresence_display_t* display = self->richpresence->richpresence->first_display; - while (display != 0) { - rc_reset_trigger(&display->trigger); - display = display->next; - } - } + if (self->richpresence && self->richpresence->richpresence) + rc_reset_richpresence(self->richpresence->richpresence); for (variable = self->variables; variable; variable = variable->next) rc_reset_value(variable); @@ -754,7 +749,7 @@ static int rc_condset_contains_memref(const rc_condset_t* condset, const rc_memr return 0; } -static int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref) { +int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref) { rc_condset_t* condset; if (!value) return 0; @@ -767,7 +762,7 @@ static int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* return 0; } -static int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref) { +int rc_trigger_contains_memref(const rc_trigger_t* trigger, const rc_memref_t* memref) { rc_condset_t* condset; if (!trigger) return 0; diff --git a/src/rcheevos/runtime_progress.c b/src/rcheevos/runtime_progress.c index cebe981c..32353faa 100644 --- a/src/rcheevos/runtime_progress.c +++ b/src/rcheevos/runtime_progress.c @@ -17,7 +17,7 @@ #define RC_RUNTIME_CHUNK_DONE 0x454E4F44 /* DONE */ typedef struct rc_runtime_progress_t { - rc_runtime_t* runtime; + const rc_runtime_t* runtime; int offset; unsigned char* buffer; @@ -81,17 +81,6 @@ static int rc_runtime_progress_match_md5(rc_runtime_progress_t* progress, unsign return result; } -static unsigned rc_runtime_progress_djb2(const char* input) -{ - unsigned result = 5381; - char c; - - while ((c = *input++) != '\0') - result = ((result << 5) + result) + c; /* result = result * 33 + c */ - - return result; -} - static void rc_runtime_progress_start_chunk(rc_runtime_progress_t* progress, unsigned chunk_id) { rc_runtime_progress_write_uint(progress, chunk_id); @@ -120,7 +109,7 @@ static void rc_runtime_progress_end_chunk(rc_runtime_progress_t* progress) } } -static void rc_runtime_progress_init(rc_runtime_progress_t* progress, rc_runtime_t* runtime, lua_State* L) +static void rc_runtime_progress_init(rc_runtime_progress_t* progress, const rc_runtime_t* runtime, lua_State* L) { memset(progress, 0, sizeof(rc_runtime_progress_t)); progress->runtime = runtime; @@ -352,7 +341,7 @@ static int rc_runtime_progress_write_variables(rc_runtime_progress_t* progress) for (variable = progress->runtime->variables; variable; variable = variable->next) { - unsigned djb2 = rc_runtime_progress_djb2(variable->name); + unsigned djb2 = rc_djb2(variable->name); rc_runtime_progress_write_uint(progress, djb2); rc_runtime_progress_write_variable(progress, variable); @@ -418,7 +407,7 @@ static int rc_runtime_progress_read_variables(rc_runtime_progress_t* progress) count = 0; for (variable = progress->runtime->variables; variable; variable = variable->next) { pending_variables[count].variable = variable; - pending_variables[count].djb2 = rc_runtime_progress_djb2(variable->name); + pending_variables[count].djb2 = rc_djb2(variable->name); ++count; } @@ -751,7 +740,7 @@ int rc_runtime_progress_size(const rc_runtime_t* runtime, lua_State* L) rc_runtime_progress_t progress; int result; - rc_runtime_progress_init(&progress, (rc_runtime_t*)runtime, L); + rc_runtime_progress_init(&progress, runtime, L); result = rc_runtime_progress_serialize_internal(&progress); if (result != RC_OK) @@ -764,7 +753,10 @@ int rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, lua { rc_runtime_progress_t progress; - rc_runtime_progress_init(&progress, (rc_runtime_t*)runtime, L); + if (!buffer) + return RC_INVALID_STATE; + + rc_runtime_progress_init(&progress, runtime, L); progress.buffer = (unsigned char*)buffer; return rc_runtime_progress_serialize_internal(&progress); @@ -782,6 +774,11 @@ int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const unsigned char* int seen_rich_presence = 0; int result = RC_OK; + if (!serialized) { + rc_runtime_reset(runtime); + return RC_INVALID_STATE; + } + rc_runtime_progress_init(&progress, runtime, L); progress.buffer = (unsigned char*)serialized; diff --git a/src/rcheevos/value.c b/src/rcheevos/value.c index 9922231d..54784caa 100644 --- a/src/rcheevos/value.c +++ b/src/rcheevos/value.c @@ -285,6 +285,20 @@ void rc_reset_value(rc_value_t* self) { self->value.changed = 0; } +int rc_value_from_hits(rc_value_t* self) +{ + rc_condset_t* condset = self->conditions; + for (; condset != NULL; condset = condset->next) { + rc_condition_t* condition = condset->conditions; + for (; condition != NULL; condition = condition->next) { + if (condition->type == RC_CONDITION_MEASURED) + return (condition->required_hits != 0); + } + } + + return 0; +} + void rc_init_parse_state_variables(rc_parse_state_t* parse, rc_value_t** variables) { parse->variables = variables; *variables = 0; diff --git a/src/rhash/hash.c b/src/rhash/hash.c index e1ff7dc7..e93423b8 100644 --- a/src/rhash/hash.c +++ b/src/rhash/hash.c @@ -2465,7 +2465,7 @@ static void rc_hash_initialize_dsk_iterator(struct rc_hash_iterator* iterator, c rc_hash_iterator_append_console(iterator, RC_CONSOLE_APPLE_II); } -void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, uint8_t* buffer, size_t buffer_size) +void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* path, const uint8_t* buffer, size_t buffer_size) { int need_path = !buffer; @@ -2598,7 +2598,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } else if (rc_path_compare_extension(ext, "d64")) { - iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; + iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; } else if (rc_path_compare_extension(ext, "d88")) { @@ -2618,7 +2618,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } else if (rc_path_compare_extension(ext, "fd")) { - iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ + iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ } break; diff --git a/test/Makefile b/test/Makefile index 67311ba6..5fddd19b 100644 --- a/test/Makefile +++ b/test/Makefile @@ -45,6 +45,7 @@ OBJ=$(RC_SRC)/alloc.o \ $(RC_SRC)/lboard.o \ $(RC_SRC)/memref.o \ $(RC_SRC)/operand.o \ + $(RC_SRC)/rc_client.o \ $(RC_SRC)/rc_libretro.o \ $(RC_SRC)/rc_validate.o \ $(RC_SRC)/richpresence.o \ @@ -68,6 +69,7 @@ OBJ=$(RC_SRC)/alloc.o \ rcheevos/test_lboard.o \ rcheevos/test_memref.o \ rcheevos/test_operand.o \ + rcheevos/test_rc_client.o \ rcheevos/test_rc_libretro.o \ rcheevos/test_rc_validate.o \ rcheevos/test_richpresence.o \ diff --git a/test/rapi/test_rc_api_runtime.c b/test/rapi/test_rc_api_runtime.c index 69fcecf1..30eda412 100644 --- a/test/rapi/test_rc_api_runtime.c +++ b/test/rapi/test_rc_api_runtime.c @@ -536,7 +536,7 @@ static void test_init_award_achievement_request_no_achievement_id() { static void test_process_award_achievement_response_success() { rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = "{\"Success\":true,\"Score\":119102,\"AchievementID\":56481,\"AchievementsRemaining\":11}"; + const char* server_response = "{\"Success\":true,\"Score\":119102,\"SoftcoreScore\":777,\"AchievementID\":56481,\"AchievementsRemaining\":11}"; memset(&award_achievement_response, 0, sizeof(award_achievement_response)); @@ -544,6 +544,7 @@ static void test_process_award_achievement_response_success() { ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); ASSERT_PTR_NULL(award_achievement_response.response.error_message); ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 119102); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 777); ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 56481); ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 11); @@ -552,7 +553,7 @@ static void test_process_award_achievement_response_success() { static void test_process_award_achievement_response_hardcore_already_unlocked() { rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":119210,\"AchievementID\":56494,\"AchievementsRemaining\":17}"; + const char* server_response = "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":119210,\"SoftcoreScore\":777,\"AchievementID\":56494,\"AchievementsRemaining\":17}"; memset(&award_achievement_response, 0, sizeof(award_achievement_response)); @@ -560,6 +561,7 @@ static void test_process_award_achievement_response_hardcore_already_unlocked() ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "User already has hardcore and regular achievements awarded."); ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 119210); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 777); ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 56494); ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 17); @@ -568,7 +570,7 @@ static void test_process_award_achievement_response_hardcore_already_unlocked() static void test_process_award_achievement_response_non_hardcore_already_unlocked() { rc_api_award_achievement_response_t award_achievement_response; - const char* server_response = "{\"Success\":false,\"Error\":\"User already has this achievement awarded.\",\"Score\":119210,\"AchievementID\":56494}"; + const char* server_response = "{\"Success\":false,\"Error\":\"User already has this achievement awarded.\",\"Score\":119210,\"SoftcoreScore\":777,\"AchievementID\":56494}"; memset(&award_achievement_response, 0, sizeof(award_achievement_response)); @@ -576,6 +578,7 @@ static void test_process_award_achievement_response_non_hardcore_already_unlocke ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "User already has this achievement awarded."); ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 119210); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 777); ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 56494); ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0xFFFFFFFF); @@ -592,6 +595,7 @@ static void test_process_award_achievement_response_generic_failure() { ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); ASSERT_PTR_NULL(award_achievement_response.response.error_message); ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); @@ -604,10 +608,11 @@ static void test_process_award_achievement_response_empty() { memset(&award_achievement_response, 0, sizeof(award_achievement_response)); - ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_INVALID_JSON); + ASSERT_NUM_EQUALS(rc_api_process_award_achievement_response(&award_achievement_response, server_response), RC_NO_RESPONSE); ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); ASSERT_PTR_NULL(award_achievement_response.response.error_message); ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); @@ -624,6 +629,7 @@ static void test_process_award_achievement_response_invalid_credentials() { ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "Credentials invalid (0)"); ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); @@ -640,6 +646,7 @@ static void test_process_award_achievement_response_text() { ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "You do not have access to that resource"); ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); @@ -656,6 +663,7 @@ static void test_process_award_achievement_response_no_fields() { ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 1); ASSERT_PTR_NULL(award_achievement_response.response.error_message); ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0xFFFFFFFF); @@ -679,6 +687,7 @@ static void test_process_award_achievement_response_429() { ASSERT_NUM_EQUALS(award_achievement_response.response.succeeded, 0); ASSERT_STR_EQUALS(award_achievement_response.response.error_message, "429 Too Many Requests"); ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score, 0); + ASSERT_UNUM_EQUALS(award_achievement_response.new_player_score_softcore, 0); ASSERT_UNUM_EQUALS(award_achievement_response.awarded_achievement_id, 0); ASSERT_UNUM_EQUALS(award_achievement_response.achievements_remaining, 0); diff --git a/test/rapi/test_rc_api_user.c b/test/rapi/test_rc_api_user.c index 89ce4376..dd1a95ba 100644 --- a/test/rapi/test_rc_api_user.c +++ b/test/rapi/test_rc_api_user.c @@ -171,7 +171,7 @@ static void test_init_login_request_alternate_host() static void test_process_login_response_success() { rc_api_login_response_t login_response; - const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"Token\":\"ApiTOKEN\",\"Score\":1234,\"Messages\":2}"; + const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"Token\":\"ApiTOKEN\",\"Score\":1234,\"SoftcoreScore\":789,\"Messages\":2}"; memset(&login_response, 0, sizeof(login_response)); @@ -181,6 +181,7 @@ static void test_process_login_response_success() ASSERT_STR_EQUALS(login_response.username, "USER"); ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); ASSERT_NUM_EQUALS(login_response.score, 1234); + ASSERT_NUM_EQUALS(login_response.score_softcore, 789); ASSERT_NUM_EQUALS(login_response.num_unread_messages, 2); ASSERT_STR_EQUALS(login_response.display_name, "USER"); @@ -190,7 +191,7 @@ static void test_process_login_response_success() static void test_process_login_response_unique_display_name() { rc_api_login_response_t login_response; - const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"DisplayName\":\"Gaming Hero\",\"Token\":\"ApiTOKEN\",\"Score\":1234,\"Messages\":2}"; + const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"DisplayName\":\"Gaming Hero\",\"Token\":\"ApiTOKEN\",\"Score\":1234,\"SoftcoreScore\":789,\"Messages\":2}"; memset(&login_response, 0, sizeof(login_response)); @@ -200,6 +201,7 @@ static void test_process_login_response_unique_display_name() ASSERT_STR_EQUALS(login_response.username, "USER"); ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); ASSERT_NUM_EQUALS(login_response.score, 1234); + ASSERT_NUM_EQUALS(login_response.score_softcore, 789); ASSERT_NUM_EQUALS(login_response.num_unread_messages, 2); ASSERT_STR_EQUALS(login_response.display_name, "Gaming Hero"); @@ -219,6 +221,7 @@ static void test_process_login_response_error() ASSERT_PTR_NULL(login_response.username); ASSERT_PTR_NULL(login_response.api_token); ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); ASSERT_PTR_NULL(login_response.display_name); @@ -238,6 +241,7 @@ static void test_process_login_response_generic_failure() ASSERT_PTR_NULL(login_response.username); ASSERT_PTR_NULL(login_response.api_token); ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); ASSERT_PTR_NULL(login_response.display_name); @@ -251,12 +255,13 @@ static void test_process_login_response_empty() memset(&login_response, 0, sizeof(login_response)); - ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_INVALID_JSON); + ASSERT_NUM_EQUALS(rc_api_process_login_response(&login_response, server_response), RC_NO_RESPONSE); ASSERT_NUM_EQUALS(login_response.response.succeeded, 0); ASSERT_PTR_NULL(login_response.response.error_message); ASSERT_PTR_NULL(login_response.username); ASSERT_PTR_NULL(login_response.api_token); ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); ASSERT_PTR_NULL(login_response.display_name); @@ -276,6 +281,7 @@ static void test_process_login_response_text() ASSERT_PTR_NULL(login_response.username); ASSERT_PTR_NULL(login_response.api_token); ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); ASSERT_PTR_NULL(login_response.display_name); @@ -295,6 +301,7 @@ static void test_process_login_response_html() ASSERT_PTR_NULL(login_response.username); ASSERT_PTR_NULL(login_response.api_token); ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); ASSERT_PTR_NULL(login_response.display_name); @@ -314,6 +321,7 @@ static void test_process_login_response_no_required_fields() ASSERT_PTR_NULL(login_response.username); ASSERT_PTR_NULL(login_response.api_token); ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); ASSERT_PTR_NULL(login_response.display_name); @@ -333,6 +341,7 @@ static void test_process_login_response_no_token() ASSERT_STR_EQUALS(login_response.username, "Username"); ASSERT_PTR_NULL(login_response.api_token); ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); ASSERT_PTR_NULL(login_response.display_name); @@ -352,6 +361,7 @@ static void test_process_login_response_no_optional_fields() ASSERT_STR_EQUALS(login_response.username, "USER"); ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); ASSERT_STR_EQUALS(login_response.display_name, "USER"); @@ -361,7 +371,7 @@ static void test_process_login_response_no_optional_fields() static void test_process_login_response_null_score() { rc_api_login_response_t login_response; - const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"Token\":\"ApiTOKEN\",\"Score\":null}"; + const char* server_response = "{\"Success\":true,\"User\":\"USER\",\"Token\":\"ApiTOKEN\",\"Score\":null,\"SoftcoreScore\":null}"; memset(&login_response, 0, sizeof(login_response)); @@ -371,6 +381,7 @@ static void test_process_login_response_null_score() ASSERT_STR_EQUALS(login_response.username, "USER"); ASSERT_STR_EQUALS(login_response.api_token, "ApiTOKEN"); ASSERT_NUM_EQUALS(login_response.score, 0); + ASSERT_NUM_EQUALS(login_response.score_softcore, 0); ASSERT_NUM_EQUALS(login_response.num_unread_messages, 0); ASSERT_STR_EQUALS(login_response.display_name, "USER"); diff --git a/test/rcheevos-test.vcxproj b/test/rcheevos-test.vcxproj index 4f98069b..64efa5de 100644 --- a/test/rcheevos-test.vcxproj +++ b/test/rcheevos-test.vcxproj @@ -141,6 +141,7 @@ + @@ -200,6 +201,7 @@ + @@ -219,6 +221,7 @@ + @@ -226,6 +229,7 @@ + diff --git a/test/rcheevos-test.vcxproj.filters b/test/rcheevos-test.vcxproj.filters index de546b97..b7bcf4e5 100644 --- a/test/rcheevos-test.vcxproj.filters +++ b/test/rcheevos-test.vcxproj.filters @@ -288,6 +288,12 @@ tests\rcheevos + + src\rcheevos + + + tests\rcheevos + @@ -362,5 +368,11 @@ src\rcheevos + + src\rcheevos + + + src\rcheevos + \ No newline at end of file diff --git a/test/rcheevos/test_consoleinfo.c b/test/rcheevos/test_consoleinfo.c index 6b8fdd63..f083e7c1 100644 --- a/test/rcheevos/test_consoleinfo.c +++ b/test/rcheevos/test_consoleinfo.c @@ -125,6 +125,7 @@ void test_consoleinfo(void) { TEST_PARAMS2(test_name, 101, "Events"); /* memory maps */ + TEST_PARAMS2(test_memory, RC_CONSOLE_UNKNOWN, 0x000000); TEST_PARAMS2(test_memory, RC_CONSOLE_3DO, 0x200000); TEST_PARAMS2(test_memory, RC_CONSOLE_AMIGA, 0x100000); TEST_PARAMS2(test_memory, RC_CONSOLE_AMSTRAD_PC, 0x090000); diff --git a/test/rcheevos/test_rc_client.c b/test/rcheevos/test_rc_client.c new file mode 100644 index 00000000..110ef9f1 --- /dev/null +++ b/test/rcheevos/test_rc_client.c @@ -0,0 +1,6373 @@ +#include "rc_client.h" + +#include "rc_consoles.h" +#include "rc_internal.h" +#include "rc_client_internal.h" +#include "rc_version.h" + +#include "../rhash/data.h" +#include "../test_framework.h" + +static rc_client_t* g_client; +static void* g_callback_userdata = &g_client; /* dummy object to use for callback userdata validation */ + +#define GENERIC_ACHIEVEMENT_JSON(id, memaddr) "{\"ID\":" id ",\"Title\":\"Achievement " id "\"," \ + "\"Description\":\"Desc " id "\",\"Flags\":3,\"Points\":5,\"MemAddr\":\"" memaddr "\"," \ + "\"Author\":\"User1\",\"BadgeName\":\"00" id "\",\"Created\":1367266583,\"Modified\":1376929305}" + +#define GENERIC_LEADERBOARD_JSON(id, memaddr, format) "{\"ID\":" id ",\"Title\":\"Leaderboard " id "\"," \ + "\"Description\":\"Desc " id "\",\"Mem\":\"" memaddr "\",\"Format\":\"" format "\"}" + +static const char* patchdata_empty = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[]" + "}}"; + +static const char* patchdata_2ach_0lbd = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0xH0001=3_0xH0002=7\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0xH0001=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," + "\"Created\":1376970283,\"Modified\":1376970283}" + "]," + "\"Leaderboards\":[]" + "}}"; + +static const char* patchdata_2ach_1lbd = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0xH0001=3_0xH0002=7\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0xH0001=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," + "\"Created\":1376970283,\"Modified\":1376970283}" + "]," + "\"Leaderboards\":[" + "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," + "\"Mem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"Format\":\"SCORE\"}" + "]" + "}}"; + +static const char* patchdata_rich_presence_only = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[]," + "\"RichPresencePatch\":\"Display:\\r\\n@Number(0xH0001)\"" + "}}"; + +static const char* patchdata_leaderboard_only = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") + "]" + "}}"; + +static const char* patchdata_bounds_check_system = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":7,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("1", "0xH0000=5") "," + GENERIC_ACHIEVEMENT_JSON("2", "0xHFFFF=5") "," + GENERIC_ACHIEVEMENT_JSON("3", "0xH10000=5") "," + GENERIC_ACHIEVEMENT_JSON("4", "0x FFFE=5") "," + GENERIC_ACHIEVEMENT_JSON("5", "0x FFFF=5") "," + GENERIC_ACHIEVEMENT_JSON("6", "0x 10000=5") + "]," + "\"Leaderboards\":[]" + "}}"; + +static const char* patchdata_bounds_check_8 = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":7,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("408", "0xH0004=5") "," + GENERIC_ACHIEVEMENT_JSON("508", "0xH0005=5") "," + GENERIC_ACHIEVEMENT_JSON("608", "0xH0006=5") "," + GENERIC_ACHIEVEMENT_JSON("708", "0xH0007=5") "," + GENERIC_ACHIEVEMENT_JSON("808", "0xH0008=5") "," + GENERIC_ACHIEVEMENT_JSON("416", "0x 0004=5") "," + GENERIC_ACHIEVEMENT_JSON("516", "0x 0005=5") "," + GENERIC_ACHIEVEMENT_JSON("616", "0x 0006=5") "," + GENERIC_ACHIEVEMENT_JSON("716", "0x 0007=5") "," + GENERIC_ACHIEVEMENT_JSON("816", "0x 0008=5") "," + GENERIC_ACHIEVEMENT_JSON("424", "0xW0004=5") "," + GENERIC_ACHIEVEMENT_JSON("524", "0xW0005=5") "," + GENERIC_ACHIEVEMENT_JSON("624", "0xW0006=5") "," + GENERIC_ACHIEVEMENT_JSON("724", "0xW0007=5") "," + GENERIC_ACHIEVEMENT_JSON("824", "0xW0008=5") "," + GENERIC_ACHIEVEMENT_JSON("432", "0xX0004=5") "," + GENERIC_ACHIEVEMENT_JSON("532", "0xX0005=5") "," + GENERIC_ACHIEVEMENT_JSON("632", "0xX0006=5") "," + GENERIC_ACHIEVEMENT_JSON("732", "0xX0007=5") "," + GENERIC_ACHIEVEMENT_JSON("832", "0xX0008=5") + "]," + "\"Leaderboards\":[]" + "}}"; + +static const char* patchdata_exhaustive = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":7,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("5", "0xH0005=5") "," + GENERIC_ACHIEVEMENT_JSON("6", "M:0xH0006=6") "," + GENERIC_ACHIEVEMENT_JSON("7", "T:0xH0007=7_0xH0001=1") "," + GENERIC_ACHIEVEMENT_JSON("8", "0xH0008=8") "," + GENERIC_ACHIEVEMENT_JSON("9", "0xH0009=9") "," + GENERIC_ACHIEVEMENT_JSON("70", "M:0xX0010=100000") "," + GENERIC_ACHIEVEMENT_JSON("71", "G:0xX0010=100000") + "]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," + GENERIC_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," /* different size */ + GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," /* different format */ + GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," /* different submit */ + GENERIC_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," /* different start */ + GENERIC_LEADERBOARD_JSON("51", "STA:0xH000A=3::CAN:0xH000C=6::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") "," /* hit count */ + GENERIC_LEADERBOARD_JSON("52", "STA:0xH000B=3::CAN:0xH000C=7::SUB:0xH000D=1::VAL:M:0xH0009=1", "VALUE") /* hit count */ + "]," + "\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"" + "}}"; + +static const char* patchdata_unofficial_unsupported = "{\"Success\":true,\"PatchData\":{" + "\"ID\":1234,\"Title\":\"Sample Game\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112233.png\"," + "\"Achievements\":[" + "{\"ID\":5501,\"Title\":\"Ach1\",\"Description\":\"Desc1\",\"Flags\":3,\"Points\":5," + "\"MemAddr\":\"0xH0001=1_0xH0002=7\",\"Author\":\"User1\",\"BadgeName\":\"00234\"," + "\"Created\":1367266583,\"Modified\":1376929305}," + "{\"ID\":5502,\"Title\":\"Ach2\",\"Description\":\"Desc2\",\"Flags\":5,\"Points\":2," + "\"MemAddr\":\"0xH0001=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00235\"," + "\"Created\":1376970283,\"Modified\":1376970283}," + "{\"ID\":5503,\"Title\":\"Ach3\",\"Description\":\"Desc3\",\"Flags\":3,\"Points\":2," + "\"MemAddr\":\"0xHFEFEFEFE=2_0x0002=9\",\"Author\":\"User1\",\"BadgeName\":\"00236\"," + "\"Created\":1376971283,\"Modified\":1376971283}" + "]," + "\"Leaderboards\":[" + "{\"ID\":4401,\"Title\":\"Leaderboard1\",\"Description\":\"Desc1\"," + "\"Mem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xHFEFEFEFE=2::VAL:0x 000E\",\"Format\":\"SCORE\"}" + "]" + "}}"; + +static const char* patchdata_subset = "{\"Success\":true,\"PatchData\":{" + "\"ID\":2345,\"Title\":\"Sample Game [Subset - Bonus]\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112234.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("7", "0xH0007=7") "," + GENERIC_ACHIEVEMENT_JSON("8", "0xH0008=8") "," + GENERIC_ACHIEVEMENT_JSON("9", "0xH0009=9") + "]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("81", "STA:0xH0008=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," + GENERIC_LEADERBOARD_JSON("82", "STA:0xH0008=2::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") + "]" + "}}"; + +static const char* patchdata_subset2 = "{\"Success\":true,\"PatchData\":{" + "\"ID\":2345,\"Title\":\"Sample Game [Subset - Multi]\",\"ConsoleID\":17,\"ImageIcon\":\"/Images/112234.png\"," + "\"Achievements\":[" + GENERIC_ACHIEVEMENT_JSON("5501", "0xH0017=7") "," + GENERIC_ACHIEVEMENT_JSON("5502", "0xH0018=8") "," + GENERIC_ACHIEVEMENT_JSON("5503", "0xH0019=9") + "]," + "\"Leaderboards\":[" + "]" + "}}"; + +static const char* no_unlocks = "{\"Success\":true,\"UserUnlocks\":[]}"; + +static const char* unlock_5501 = "{\"Success\":true,\"UserUnlocks\":[5501]}"; +static const char* unlock_5502 = "{\"Success\":true,\"UserUnlocks\":[5502]}"; +static const char* unlock_5501_and_5502 = "{\"Success\":true,\"UserUnlocks\":[5501,5502]}"; +static const char* unlock_8 = "{\"Success\":true,\"UserUnlocks\":[8]}"; +static const char* unlock_6_8_and_9 = "{\"Success\":true,\"UserUnlocks\":[6,8,9]}"; + +static const char* response_429 = + "\n" + "429 Too Many Requests\n" + "\n" + "

429 Too Many Requests

\n" + "
nginx
\n" + "\n" + ""; + +/* ----- helpers ----- */ + +static void _assert_achievement_state(rc_client_t* client, uint32_t id, int expected_state) +{ + const rc_client_achievement_t* achievement = rc_client_get_achievement_info(client, id); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, expected_state); +} +#define assert_achievement_state(client, id, expected_state) ASSERT_HELPER(_assert_achievement_state(client, id, expected_state), "assert_achievement_state") + +static rc_client_event_t events[16]; +static int event_count = 0; + +static void rc_client_event_handler(const rc_client_event_t* e, rc_client_t* client) +{ + memcpy(&events[event_count++], e, sizeof(rc_client_event_t)); + + if (e->type == RC_CLIENT_EVENT_SERVER_ERROR) { + static char event_server_error_message[128]; + static rc_client_server_error_t event_server_error; + + /* server error data is not maintained out of scope, copy it too */ + memcpy(&event_server_error, e->server_error, sizeof(event_server_error)); + strcpy_s(event_server_error_message, sizeof(event_server_error_message), e->server_error->error_message); + event_server_error.error_message = event_server_error_message; + events[event_count - 1].server_error = &event_server_error; + } +} + +static rc_client_event_t* find_event(uint8_t type, uint32_t id) +{ + int i; + + for (i = 0; i < event_count; ++i) { + if (events[i].type == type) { + switch (type) { + case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: + if (events[i].achievement->id == id) + return &events[i]; + break; + + case RC_CLIENT_EVENT_LEADERBOARD_STARTED: + case RC_CLIENT_EVENT_LEADERBOARD_FAILED: + case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: + if (events[i].leaderboard->id == id) + return &events[i]; + break; + + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: + if (events[i].leaderboard_tracker->id == id) + return &events[i]; + break; + + case RC_CLIENT_EVENT_GAME_COMPLETED: + case RC_CLIENT_EVENT_RESET: + case RC_CLIENT_EVENT_SERVER_ERROR: + return &events[i]; + + default: + break; + } + } + } + + return NULL; +} + +static uint8_t* g_memory = NULL; +static uint32_t g_memory_size = 0; + +static void mock_memory(uint8_t* memory, uint32_t size) +{ + g_memory = memory; + g_memory_size = size; +} + +static uint32_t rc_client_read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) +{ + if (g_memory_size > 0) { + if (address >= g_memory_size) + return 0; + + uint32_t num_avail = g_memory_size - address; + if (num_avail < num_bytes) + num_bytes = num_avail; + + memcpy(buffer, &g_memory[address], num_bytes); + return num_bytes; + } + + memset(&buffer, 0, num_bytes); + return num_bytes; +} + +/* ----- API mocking ----- */ + +typedef struct rc_mock_api_response +{ + const char* request_params; + rc_api_server_response_t server_response; + int seen; + rc_client_server_callback_t async_callback; + void* async_callback_data; +} rc_mock_api_response; + +static rc_mock_api_response g_mock_api_responses[12]; +static int g_num_mock_api_responses = 0; + +static void rc_client_server_call(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) +{ + rc_api_server_response_t server_response; + + int i; + for (i = 0; i < g_num_mock_api_responses; i++) { + if (strcmp(g_mock_api_responses[i].request_params, request->post_data) == 0) { + g_mock_api_responses[i].seen++; + callback(&g_mock_api_responses[i].server_response, callback_data); + return; + } + } + + ASSERT_FAIL("No API response for: %s", request->post_data); + + /* still call the callback to prevent memory leak */ + memset(&server_response, 0, sizeof(server_response)); + server_response.body = ""; + server_response.http_status_code = 500; + callback(&server_response, callback_data); +} + +static void rc_client_server_call_async(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) +{ + g_mock_api_responses[g_num_mock_api_responses].request_params = strdup(request->post_data); + g_mock_api_responses[g_num_mock_api_responses].async_callback = callback; + g_mock_api_responses[g_num_mock_api_responses].async_callback_data = callback_data; + g_mock_api_responses[g_num_mock_api_responses].seen = -1; + g_num_mock_api_responses++; +} + +static void _async_api_response(const char* request_params, const char* response_body, int http_status_code) +{ + int i; + for (i = 0; i < g_num_mock_api_responses; i++) + { + if (g_mock_api_responses[i].request_params && strcmp(g_mock_api_responses[i].request_params, request_params) == 0) + { + g_mock_api_responses[i].seen++; + g_mock_api_responses[i].server_response.body = response_body; + g_mock_api_responses[i].server_response.body_length = strlen(response_body); + g_mock_api_responses[i].server_response.http_status_code = http_status_code; + g_mock_api_responses[i].async_callback(&g_mock_api_responses[i].server_response, g_mock_api_responses[i].async_callback_data); + free((void*)g_mock_api_responses[i].request_params); + g_mock_api_responses[i].request_params = NULL; + + while (g_num_mock_api_responses > 0 && g_mock_api_responses[g_num_mock_api_responses - 1].request_params == NULL) + --g_num_mock_api_responses; + return; + } + } + + ASSERT_FAIL("No pending API request for: %s", request_params); +} + +static void async_api_response(const char* request_params, const char* response_body) +{ + _async_api_response(request_params, response_body, 200); +} + +static void async_api_error(const char* request_params, const char* response_body, int http_status_code) +{ + _async_api_response(request_params, response_body, http_status_code); +} + +static void _assert_api_called(const char* request_params, int count) +{ + int i; + for (i = 0; i < g_num_mock_api_responses; i++) { + if (g_mock_api_responses[i].request_params && + strcmp(g_mock_api_responses[i].request_params, request_params) == 0) { + ASSERT_NUM_EQUALS(g_mock_api_responses[i].seen, count); + return; + } + } + + ASSERT_NUM_EQUALS(0, count); +} +#define assert_api_called(request_params) ASSERT_HELPER(_assert_api_called(request_params, 1), "assert_api_called") +#define assert_api_not_called(request_params) ASSERT_HELPER(_assert_api_called(request_params, 0), "assert_api_not_called") +#define assert_api_call_count(request_params, num) ASSERT_HELPER(_assert_api_called(request_params, num), "assert_api_call_count") +#define assert_api_pending(request_params) ASSERT_HELPER(_assert_api_called(request_params, -1), "assert_api_pending") + +static void reset_mock_api_handlers(void) +{ + g_num_mock_api_responses = 0; + memset(g_mock_api_responses, 0, sizeof(g_mock_api_responses)); +} + +static void mock_api_response(const char* request_params, const char* response_body) +{ + g_mock_api_responses[g_num_mock_api_responses].request_params = request_params; + g_mock_api_responses[g_num_mock_api_responses].server_response.body = response_body; + g_mock_api_responses[g_num_mock_api_responses].server_response.body_length = strlen(response_body); + g_mock_api_responses[g_num_mock_api_responses].server_response.http_status_code = 200; + g_num_mock_api_responses++; +} + +static void mock_api_error(const char* request_params, const char* response_body, int http_status_code) +{ + g_mock_api_responses[g_num_mock_api_responses].request_params = request_params; + g_mock_api_responses[g_num_mock_api_responses].server_response.body = response_body; + g_mock_api_responses[g_num_mock_api_responses].server_response.body_length = strlen(response_body); + g_mock_api_responses[g_num_mock_api_responses].server_response.http_status_code = http_status_code; + g_num_mock_api_responses++; +} + +static void rc_client_callback_expect_success(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_OK); + ASSERT_PTR_NULL(error_message); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_callback_expect_uncalled(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_FAIL("Callback should not have been called."); +} + +static rc_client_t* mock_client_not_logged_in(void) +{ + mock_memory(NULL, 0); + rc_api_set_host(NULL); + reset_mock_api_handlers(); + return rc_client_create(rc_client_read_memory, rc_client_server_call); +} + +static rc_client_t* mock_client_not_logged_in_async(void) +{ + mock_memory(NULL, 0); + rc_api_set_host(NULL); + return rc_client_create(rc_client_read_memory, rc_client_server_call_async); +} + +static rc_client_t* mock_client_logged_in(void) +{ + rc_client_t* client = rc_client_create(rc_client_read_memory, rc_client_server_call); + client->user.username = "Username"; + client->user.display_name = "DisplayName"; + client->user.token = "ApiToken"; + client->user.score = 12345; + client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN; + + rc_client_set_event_handler(client, rc_client_event_handler); + reset_mock_api_handlers(); + + mock_memory(NULL, 0); + rc_api_set_host(NULL); + return client; +} + +static void mock_client_load_game(const char* patchdata, const char* hardcore_unlocks, const char* softcore_unlocks) +{ + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", softcore_unlocks); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", hardcore_unlocks); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + if (!g_client->game) + ASSERT_MESSAGE("client->game is NULL"); +} + +static rc_client_t* mock_client_game_loaded(const char* patchdata, const char* hardcore_unlocks, const char* softcore_unlocks) +{ + g_client = mock_client_logged_in(); + + mock_client_load_game(patchdata, hardcore_unlocks, softcore_unlocks); + + return g_client; +} + +static void mock_client_load_subset(const char* patchdata, const char* hardcore_unlocks, const char* softcore_unlocks) +{ + mock_api_response("r=patch&u=Username&t=ApiToken&g=2345", patchdata); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=2345&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=2345&h=0", softcore_unlocks); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=2345&h=1", hardcore_unlocks); + + rc_client_begin_load_subset(g_client, 2345, rc_client_callback_expect_success, g_callback_userdata); +} + +/* ----- login ----- */ + +static void test_login_with_password(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_response("r=login&u=User&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "User"); + ASSERT_STR_EQUALS(user->display_name, "User"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->score_softcore, 123); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + + rc_client_destroy(g_client); +} + +static void test_login_with_token(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_response("r=login&u=User&t=ApiToken", + "{\"Success\":true,\"User\":\"User\",\"DisplayName\":\"Display\",\"Token\":\"ApiToken\",\"Score\":12345,\"Messages\":2}"); + + rc_client_begin_login_with_token(g_client, "User", "ApiToken", rc_client_callback_expect_success, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "User"); + ASSERT_STR_EQUALS(user->display_name, "Display"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_username_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "username is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_callback_expect_password_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "password is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void rc_client_callback_expect_token_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "token is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_login_required_fields(void) +{ + g_client = mock_client_not_logged_in(); + + rc_client_begin_login_with_password(g_client, "User", "", rc_client_callback_expect_password_required, g_callback_userdata); + rc_client_begin_login_with_password(g_client, "", "Pa$$word", rc_client_callback_expect_username_required, g_callback_userdata); + rc_client_begin_login_with_password(g_client, "", "", rc_client_callback_expect_username_required, g_callback_userdata); + + rc_client_begin_login_with_token(g_client, "User", "", rc_client_callback_expect_token_required, g_callback_userdata); + rc_client_begin_login_with_token(g_client, "", "ApiToken", rc_client_callback_expect_username_required, g_callback_userdata); + rc_client_begin_login_with_token(g_client, "", "", rc_client_callback_expect_username_required, g_callback_userdata); + + ASSERT_NUM_EQUALS(g_client->state.user, RC_CLIENT_USER_STATE_NONE); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_credentials_error(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_API_FAILURE); + ASSERT_STR_EQUALS(error_message, "Invalid User/Password combination. Please try again"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_login_with_incorrect_password(void) +{ + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_error("r=login&u=User&p=Pa%24%24word", "{\"Success\":false,\"Error\":\"Invalid User/Password combination. Please try again\"}", 403); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_credentials_error, g_callback_userdata); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_missing_token(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_MISSING_VALUE); + ASSERT_STR_EQUALS(error_message, "Token not found in response"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_login_incomplete_response(void) +{ + g_client = mock_client_not_logged_in(); + reset_mock_api_handlers(); + mock_api_response("r=login&u=User&p=Pa%24%24word", "{\"Success\":true,\"User\":\"Username\"}"); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_missing_token, g_callback_userdata); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void test_login_with_password_async(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NULL(user); + + async_api_response("r=login&u=User&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + ASSERT_STR_EQUALS(user->username, "User"); + ASSERT_STR_EQUALS(user->display_name, "User"); + ASSERT_STR_EQUALS(user->token, "ApiToken"); + ASSERT_NUM_EQUALS(user->score, 12345); + ASSERT_NUM_EQUALS(user->num_unread_messages, 2); + + rc_client_destroy(g_client); +} + +static void test_login_with_password_async_aborted(void) +{ + const rc_client_user_t* user; + rc_client_async_handle_t* handle; + + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + handle = rc_client_begin_login_with_password(g_client, "User", "Pa$$word", + rc_client_callback_expect_uncalled, g_callback_userdata); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NULL(user); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=login&u=User&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NULL(user); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_login_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_LOGIN_REQUIRED); + ASSERT_STR_EQUALS(error_message, "Login required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_logout(void) +{ + const rc_client_user_t* user; + + g_client = mock_client_logged_in(); + + user = rc_client_get_user_info(g_client); + ASSERT_PTR_NOT_NULL(user); + + rc_client_logout(g_client); + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + /* reference pointer should be NULLed out */ + ASSERT_PTR_NULL(user->display_name); + ASSERT_PTR_NULL(user->username); + ASSERT_PTR_NULL(user->token); + + /* attempt to load game should fail */ + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_logout_with_game_loaded(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(rc_client_get_user_info(g_client)); + ASSERT_PTR_NOT_NULL(rc_client_get_game_info(g_client)); + + rc_client_logout(g_client); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + ASSERT_PTR_NULL(rc_client_get_game_info(g_client)); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_login_aborted(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_ABORTED); + ASSERT_STR_EQUALS(error_message, "Login aborted"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_logout_during_login(void) +{ + g_client = mock_client_not_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + rc_client_begin_login_with_password(g_client, "User", "Pa$$word", rc_client_callback_expect_login_aborted, g_callback_userdata); + rc_client_logout(g_client); + + async_api_response("r=login&u=User&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"User\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_no_longer_active(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_ABORTED); + ASSERT_STR_EQUALS(error_message, "The requested game is no longer active"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_logout_during_fetch_game(void) +{ + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_no_longer_active, g_callback_userdata); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + async_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_logout(g_client); + + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", "{\"Success\":true,\"UserUnlocks\":[]}"); + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", "{\"Success\":true,\"UserUnlocks\":[]}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + ASSERT_PTR_NULL(rc_client_get_user_info(g_client)); + + rc_client_destroy(g_client); +} + +static void test_user_get_image_url(void) +{ + char buffer[256]; + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + + ASSERT_NUM_EQUALS(rc_client_user_get_image_url(rc_client_get_user_info(g_client), buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/UserPic/DisplayName.png"); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_exhaustive, unlock_8, unlock_6_8_and_9); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 1); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 5); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_softcore(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_exhaustive, unlock_8, unlock_6_8_and_9); + rc_client_set_hardcore_enabled(g_client, 0); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 3); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 15); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_encore_mode(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_exhaustive); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", unlock_6_8_and_9); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", unlock_8); + + rc_client_set_encore_mode_enabled(g_client, 1); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 7); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 0); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 1); + + ASSERT_NUM_EQUALS(summary.points_core, 35); + ASSERT_NUM_EQUALS(summary.points_unlocked, 5); + + rc_client_destroy(g_client); +} + +static void test_get_user_game_summary_with_unsupported_and_unofficial(void) +{ + rc_client_user_game_summary_t summary; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks, no_unlocks); + + rc_client_get_user_game_summary(g_client, &summary); + ASSERT_NUM_EQUALS(summary.num_core_achievements, 2); + ASSERT_NUM_EQUALS(summary.num_unofficial_achievements, 1); + ASSERT_NUM_EQUALS(summary.num_unsupported_achievements, 1); + ASSERT_NUM_EQUALS(summary.num_unlocked_achievements, 0); + + ASSERT_NUM_EQUALS(summary.points_core, 7); + ASSERT_NUM_EQUALS(summary.points_unlocked, 0); + + rc_client_destroy(g_client); +} + + +/* ----- load game ----- */ + +static void rc_client_callback_expect_hash_required(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "hash is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_load_game_required_fields(void) +{ + g_client = mock_client_logged_in(); + + rc_client_begin_load_game(g_client, NULL, rc_client_callback_expect_hash_required, g_callback_userdata); + rc_client_begin_load_game(g_client, "", rc_client_callback_expect_hash_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_unknown_game(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_NO_GAME_LOADED); + ASSERT_STR_EQUALS(error_message, "Unknown game"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_load_game_unknown_hash(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":0}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, RC_CONSOLE_UNKNOWN); + ASSERT_STR_EQUALS(g_client->game->public.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, ""); + } + rc_client_destroy(g_client); +} + +static void test_load_game_not_logged_in(void) +{ + g_client = mock_client_not_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game(void) +{ + rc_client_achievement_info_t* achievement; + rc_client_leaderboard_info_t* leaderboard; + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", no_unlocks); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", no_unlocks); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + + achievement = &g_client->game->subsets->achievements[0]; + ASSERT_NUM_EQUALS(achievement->public.id, 5501); + ASSERT_STR_EQUALS(achievement->public.title, "Ach1"); + ASSERT_STR_EQUALS(achievement->public.description, "Desc1"); + ASSERT_STR_EQUALS(achievement->public.badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->public.points, 5); + ASSERT_NUM_EQUALS(achievement->public.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &g_client->game->subsets->achievements[1]; + ASSERT_NUM_EQUALS(achievement->public.id, 5502); + ASSERT_STR_EQUALS(achievement->public.title, "Ach2"); + ASSERT_STR_EQUALS(achievement->public.description, "Desc2"); + ASSERT_STR_EQUALS(achievement->public.badge_name, "00235"); + ASSERT_NUM_EQUALS(achievement->public.points, 2); + ASSERT_NUM_EQUALS(achievement->public.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + leaderboard = &g_client->game->subsets->leaderboards[0]; + ASSERT_NUM_EQUALS(leaderboard->public.id, 4401); + ASSERT_STR_EQUALS(leaderboard->public.title, "Leaderboard1"); + ASSERT_STR_EQUALS(leaderboard->public.description, "Desc1"); + ASSERT_NUM_EQUALS(leaderboard->public.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + } + + rc_client_destroy(g_client); +} + +static void test_load_game_async_login(void) +{ + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + rc_client_begin_login_with_password(g_client, "Username", "Pa$$word", rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + /* game load process will stop here waiting for the login to complete */ + assert_api_not_called("r=patch&u=Username&t=ApiToken&g=1234"); + + /* login completion will trigger process to continue */ + async_api_response("r=login&u=Username&p=Pa%24%24word", + "{\"Success\":true,\"User\":\"Username\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); + assert_api_pending("r=patch&u=Username&t=ApiToken&g=1234"); + + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + async_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", no_unlocks); + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", no_unlocks); + + ASSERT_STR_EQUALS(g_client->user.username, "Username"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + rc_client_destroy(g_client); +} + +static void test_load_game_async_login_with_incorrect_password(void) +{ + g_client = mock_client_not_logged_in_async(); + reset_mock_api_handlers(); + + rc_client_begin_login_with_password(g_client, "Username", "Pa$$word", rc_client_callback_expect_credentials_error, g_callback_userdata); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_required, g_callback_userdata); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + /* game load process will stop here waiting for the login to complete */ + assert_api_not_called("r=patch&u=Username&t=ApiToken&g=1234"); + + /* login failure will trigger process to continue */ + async_api_error("r=login&u=Username&p=Pa%24%24word", + "{\"Success\":false,\"Error\":\"Invalid User/Password combination. Please try again\"}", 403); + assert_api_not_called("r=patch&u=Username&t=ApiToken&g=1234"); + + ASSERT_PTR_NULL(g_client->user.username); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_too_many_requests(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_JSON); + ASSERT_STR_EQUALS(error_message, "429 Too Many Requests"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_load_game_gameid_failure(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_error("r=gameid&m=0123456789ABCDEF", response_429, 429); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", "{\"Success\":true,\"UserUnlocks\":[]}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", "{\"Success\":true,\"UserUnlocks\":[]}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_patch_failure(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_error("r=patch&u=Username&t=ApiToken&g=1234", response_429, 429); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", "{\"Success\":true,\"UserUnlocks\":[]}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", "{\"Success\":true,\"UserUnlocks\":[]}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_postactivity_failure(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_error("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, response_429, 429); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", "{\"Success\":true,\"UserUnlocks\":[]}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", "{\"Success\":true,\"UserUnlocks\":[]}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_softcore_unlocks_failure(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_error("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", response_429, 429); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", "{\"Success\":true,\"UserUnlocks\":[]}"); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_hardcore_unlocks_failure(void) +{ + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", "{\"Success\":true,\"UserUnlocks\":[]}"); + mock_api_error("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", response_429, 429); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_gameid_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_uncalled, g_callback_userdata); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + assert_api_not_called("r=patch&u=Username&t=ApiToken&g=1234"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_patch_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_uncalled, g_callback_userdata); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + assert_api_not_called("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_postactivity_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_uncalled, g_callback_userdata); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", "{\"Success\":true,\"UserUnlocks\":[]}"); + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", "{\"Success\":true,\"UserUnlocks\":[]}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_softcore_unlocks_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_uncalled, g_callback_userdata); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + async_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", "{\"Success\":true,\"UserUnlocks\":[]}"); + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", "{\"Success\":true,\"UserUnlocks\":[]}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_hardcore_unlocks_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", + rc_client_callback_expect_uncalled, g_callback_userdata); + + async_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + async_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", "{\"Success\":true,\"UserUnlocks\":[]}"); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", "{\"Success\":true,\"UserUnlocks\":[]}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_load_game_while_spectating(void) +{ + rc_client_achievement_info_t* achievement; + rc_client_leaderboard_info_t* leaderboard; + g_client = mock_client_logged_in(); + rc_client_set_spectator_mode_enabled(g_client, 1); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + /* spectator mode should not start a session or fetch unlocks */ + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + + achievement = &g_client->game->subsets->achievements[0]; + ASSERT_NUM_EQUALS(achievement->public.id, 5501); + ASSERT_STR_EQUALS(achievement->public.title, "Ach1"); + ASSERT_STR_EQUALS(achievement->public.description, "Desc1"); + ASSERT_STR_EQUALS(achievement->public.badge_name, "00234"); + ASSERT_NUM_EQUALS(achievement->public.points, 5); + ASSERT_NUM_EQUALS(achievement->public.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &g_client->game->subsets->achievements[1]; + ASSERT_NUM_EQUALS(achievement->public.id, 5502); + ASSERT_STR_EQUALS(achievement->public.title, "Ach2"); + ASSERT_STR_EQUALS(achievement->public.description, "Desc2"); + ASSERT_STR_EQUALS(achievement->public.badge_name, "00235"); + ASSERT_NUM_EQUALS(achievement->public.points, 2); + ASSERT_NUM_EQUALS(achievement->public.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + leaderboard = &g_client->game->subsets->leaderboards[0]; + ASSERT_NUM_EQUALS(leaderboard->public.id, 4401); + ASSERT_STR_EQUALS(leaderboard->public.title, "Leaderboard1"); + ASSERT_STR_EQUALS(leaderboard->public.description, "Desc1"); + ASSERT_NUM_EQUALS(leaderboard->public.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + } + + /* spectator mode cannot be disabled if it was enabled before loading the game */ + rc_client_set_spectator_mode_enabled(g_client, 0); + ASSERT_TRUE(rc_client_get_spectator_mode_enabled(g_client)); + + rc_client_unload_game(g_client); + + /* spectator mode can be disabled after unloading game */ + rc_client_set_spectator_mode_enabled(g_client, 0); + ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); + + rc_client_destroy(g_client); +} + +/* ----- identify and load game ----- */ + +static void rc_client_callback_expect_data_or_file_path_required(int result, const char* error_message, rc_client_t* client, void* callback_data) +{ + ASSERT_NUM_EQUALS(result, RC_INVALID_STATE); + ASSERT_STR_EQUALS(error_message, "either data or file_path is required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_data, g_callback_userdata); +} + +static void test_identify_and_load_game_required_fields(void) +{ + g_client = mock_client_logged_in(); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, NULL, NULL, 0, + rc_client_callback_expect_data_or_file_path_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); +} + +static void test_identify_and_load_game_console_specified(void) +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", no_unlocks); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", no_unlocks); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_NINTENDO, "foo.zip#foo.nes", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_console_not_specified(void) +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", no_unlocks); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", no_unlocks); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.nes", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_unknown_hash(void) +{ + size_t image_size; + uint8_t* image = generate_nes_file(32, 1, &image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.nes", + image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, RC_CONSOLE_NINTENDO); + ASSERT_STR_EQUALS(g_client->game->public.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, ""); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_multihash(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", no_unlocks); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", no_unlocks); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_multihash_unknown_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", + image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 0); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, RC_CONSOLE_APPLE_II); + ASSERT_STR_EQUALS(g_client->game->public.title, "Unknown Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, ""); + } + + /* same hash generated for all dsk consoles - only one server call should be made */ + assert_api_call_count("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", 1); + + rc_client_destroy(g_client); + free(image); +} + +static void test_identify_and_load_game_multihash_differ(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", + image, image_size, rc_client_callback_expect_success, g_callback_userdata); + + /* modify the checksum so callback for first lookup will generate a new lookup */ + memset(&image[256], 0, 32); + + /* first lookup fails */ + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); + ASSERT_PTR_NOT_NULL(g_client->state.load); + + /* second lookup should succeed */ + async_api_response("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", "{\"Success\":true,\"GameID\":1234}"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + async_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", no_unlocks); + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", no_unlocks); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "4989b063a40dcfa28291ff8d675050e3"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +/* ----- change media ----- */ + +static void test_change_media_required_fields(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + + rc_client_begin_change_media(g_client, NULL, NULL, 0, + rc_client_callback_expect_data_or_file_path_required, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void rc_client_callback_expect_no_game_loaded(int result, const char* error_message, rc_client_t* client, void* callback_data) +{ + ASSERT_NUM_EQUALS(result, RC_NO_GAME_LOADED); + ASSERT_STR_EQUALS(error_message, "No game loaded"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_data, g_callback_userdata); +} + +static void test_change_media_no_game_loaded(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_no_game_loaded, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_same_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + /* changing known discs within a game set is expected to succeed */ + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + /* resetting with a disc from the current game is allowed */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_known_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":5555}"); + + /* changing to a known disc from another game is allowed */ + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + /* resetting with a disc from another game will disable the client */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void rc_client_callback_expect_hardcore_disabled_undentified_media(int result, const char* error_message, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_HARDCORE_DISABLED); + ASSERT_STR_EQUALS(error_message, "Hardcore disabled. Unidentified media inserted."); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); +} + +static void test_change_media_unknown_game(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + ASSERT_TRUE(rc_client_get_hardcore_enabled(g_client)); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":0}"); + + /* changing to an unknown disc is not allowed - could be a hacked version of one of the game's discs */ + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_hardcore_disabled_undentified_media, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + ASSERT_FALSE(rc_client_get_hardcore_enabled(g_client)); + + /* resetting with a disc not from the current game will disable the client */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_unhashable(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + + /* N64 hash will fail with Not a Nintendo 64 ROM */ + g_client->game->public.console_id = RC_CONSOLE_NINTENDO_64; + + /* changing to a disc not supported by the system is allowed */ + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "[NO HASH]"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + /* resetting with a disc not from the current game will disable the client */ + rc_client_reset(g_client); + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NULL(g_client->game); + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_back_and_forth(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + uint8_t* image2 = generate_generic_file(image_size); + memset(&image2[256], 0, 32); /* force image2 to be different */ + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + + mock_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", "{\"Success\":true,\"GameID\":1234}"); + + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_change_media(g_client, "foo.zip#foo2.nes", image2, image_size, + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_change_media(g_client, "foo.zip#foo2.nes", image2, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + assert_api_call_count("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", 1); + assert_api_call_count("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", 1); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "4989b063a40dcfa28291ff8d675050e3"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image2); + free(image); +} + +static void test_change_media_while_loading(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "4989b063a40dcfa28291ff8d675050e3", + rc_client_callback_expect_success, g_callback_userdata); + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + /* load game lookup */ + async_api_response("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", "{\"Success\":true,\"GameID\":1234}"); + + /* media request won't occur until patch data is received */ + assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + + /* finish loading game */ + async_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", no_unlocks); + assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", no_unlocks); + + /* secondary hash resolution does not occur until game is fully loaded or hash can't be compared to loaded game */ + assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_while_loading_later(void) +{ + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_logged_in(); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + rc_client_begin_load_game(g_client, "4989b063a40dcfa28291ff8d675050e3", + rc_client_callback_expect_success, g_callback_userdata); + + /* get past fetching the patch data so there's a valid console for the change media call */ + async_api_response("r=gameid&m=4989b063a40dcfa28291ff8d675050e3", "{\"Success\":true,\"GameID\":1234}"); + async_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + + /* change_media should immediately attempt to resolve the new hash */ + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + assert_api_pending("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + + /* finish loading game */ + async_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", no_unlocks); + async_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", no_unlocks); + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "6a2305a2b6675a97ff792709be1ca857"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + rc_client_destroy(g_client); + free(image); +} + +static void test_change_media_aborted(void) +{ + rc_client_async_handle_t* handle; + const size_t image_size = 32768; + uint8_t* image = generate_generic_file(image_size); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + reset_mock_api_handlers(); + + /* changing known discs within a game set is expected to succeed */ + handle = rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_uncalled, g_callback_userdata); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=gameid&m=6a2305a2b6675a97ff792709be1ca857", "{\"Success\":true,\"GameID\":1234}"); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "0123456789ABCDEF"); /* old hash retained */ + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + /* hash should still have been captured and lookup should succeed without having to call server again */ + reset_mock_api_handlers(); + + rc_client_begin_change_media(g_client, "foo.zip#foo.nes", image, image_size, + rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_STR_EQUALS(g_client->game->public.hash, "6a2305a2b6675a97ff792709be1ca857"); + assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); + + rc_client_destroy(g_client); + free(image); +} + +/* ----- get game image ----- */ + +static void test_game_get_image_url(void) +{ + char buffer[256]; + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + + ASSERT_NUM_EQUALS(rc_client_game_get_image_url(rc_client_get_game_info(g_client), buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Images/112233.png"); + + rc_client_destroy(g_client); +} + +static void test_game_get_image_url_non_ssl(void) +{ + char buffer[256]; + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + rc_client_set_host(g_client, "http://retroachievements.org"); + + ASSERT_NUM_EQUALS(rc_client_game_get_image_url(rc_client_get_game_info(g_client), buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "http://media.retroachievements.org/Images/112233.png"); + + rc_client_destroy(g_client); +} + +static void test_game_get_image_url_custom(void) +{ + char buffer[256]; + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + rc_client_set_host(g_client, "localhost"); + + ASSERT_NUM_EQUALS(rc_client_game_get_image_url(rc_client_get_game_info(g_client), buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "http://localhost/Images/112233.png"); + + rc_client_destroy(g_client); +} + +/* ----- subset ----- */ + +static void test_load_subset(void) +{ + rc_client_achievement_info_t* achievement; + rc_client_leaderboard_info_t* leaderboard; + rc_client_subset_info_t* subset_info; + const rc_client_subset_t* subset; + g_client = mock_client_logged_in(); + + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", no_unlocks); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", no_unlocks); + + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + mock_api_response("r=patch&u=Username&t=ApiToken&g=2345", patchdata_subset); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=2345&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=2345&h=0", no_unlocks); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=2345&h=1", no_unlocks); + + rc_client_begin_load_subset(g_client, 2345, rc_client_callback_expect_success, g_callback_userdata); + + ASSERT_PTR_NULL(g_client->state.load); + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + ASSERT_PTR_EQUALS(rc_client_get_game_info(g_client), &g_client->game->public); + + ASSERT_NUM_EQUALS(g_client->game->public.id, 1234); + ASSERT_NUM_EQUALS(g_client->game->public.console_id, 17); + ASSERT_STR_EQUALS(g_client->game->public.title, "Sample Game"); + ASSERT_STR_EQUALS(g_client->game->public.hash, "0123456789ABCDEF"); + ASSERT_STR_EQUALS(g_client->game->public.badge_name, "112233"); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_achievements, 2); + ASSERT_NUM_EQUALS(g_client->game->subsets->public.num_leaderboards, 1); + } + + subset = rc_client_get_subset_info(g_client, 2345); + ASSERT_PTR_NOT_NULL(subset); + if (subset) { + subset_info = g_client->game->subsets->next; + ASSERT_PTR_EQUALS(subset, &subset_info->public); + + ASSERT_NUM_EQUALS(subset->id, 2345); + ASSERT_STR_EQUALS(subset->title, "Bonus"); + ASSERT_STR_EQUALS(subset->badge_name, "112234"); + ASSERT_NUM_EQUALS(subset->num_achievements, 3); + ASSERT_NUM_EQUALS(subset->num_leaderboards, 2); + + achievement = &subset_info->achievements[0]; + ASSERT_NUM_EQUALS(achievement->public.id, 7); + ASSERT_STR_EQUALS(achievement->public.title, "Achievement 7"); + ASSERT_STR_EQUALS(achievement->public.description, "Desc 7"); + ASSERT_STR_EQUALS(achievement->public.badge_name, "007"); + ASSERT_NUM_EQUALS(achievement->public.points, 5); + ASSERT_NUM_EQUALS(achievement->public.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &subset_info->achievements[1]; + ASSERT_NUM_EQUALS(achievement->public.id, 8); + ASSERT_STR_EQUALS(achievement->public.title, "Achievement 8"); + ASSERT_STR_EQUALS(achievement->public.description, "Desc 8"); + ASSERT_STR_EQUALS(achievement->public.badge_name, "008"); + ASSERT_NUM_EQUALS(achievement->public.points, 5); + ASSERT_NUM_EQUALS(achievement->public.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + achievement = &subset_info->achievements[2]; + ASSERT_NUM_EQUALS(achievement->public.id, 9); + ASSERT_STR_EQUALS(achievement->public.title, "Achievement 9"); + ASSERT_STR_EQUALS(achievement->public.description, "Desc 9"); + ASSERT_STR_EQUALS(achievement->public.badge_name, "009"); + ASSERT_NUM_EQUALS(achievement->public.points, 5); + ASSERT_NUM_EQUALS(achievement->public.unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->public.state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->public.category, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE); + ASSERT_PTR_NOT_NULL(achievement->trigger); + + leaderboard = &subset_info->leaderboards[0]; + ASSERT_NUM_EQUALS(leaderboard->public.id, 81); + ASSERT_STR_EQUALS(leaderboard->public.title, "Leaderboard 81"); + ASSERT_STR_EQUALS(leaderboard->public.description, "Desc 81"); + ASSERT_NUM_EQUALS(leaderboard->public.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + + leaderboard = &subset_info->leaderboards[1]; + ASSERT_NUM_EQUALS(leaderboard->public.id, 82); + ASSERT_STR_EQUALS(leaderboard->public.title, "Leaderboard 82"); + ASSERT_STR_EQUALS(leaderboard->public.description, "Desc 82"); + ASSERT_NUM_EQUALS(leaderboard->public.state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_FORMAT_SCORE); + ASSERT_PTR_NOT_NULL(leaderboard->lboard); + ASSERT_NUM_NOT_EQUALS(leaderboard->value_djb2, 0); + ASSERT_PTR_NULL(leaderboard->tracker); + } + + rc_client_destroy(g_client); +} + +/* ----- achievement list ----- */ + +static void test_achievement_list_simple(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t** iter; + rc_client_achievement_t* achievement; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 0); + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_simple_with_unlocks(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t** iter; + rc_client_achievement_t* achievement; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501, unlock_5501_and_5502); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + /* in hardcore mode, 5501 should be unlocked, but 5502 will be locked */ + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + iter = list->buckets[1].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + + rc_client_destroy_achievement_list(list); + } + + rc_client_set_hardcore_enabled(g_client, 0); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + /* in softcore mode, both should be unlocked */ + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_simple_with_unlocks_encore_mode(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t** iter; + rc_client_achievement_t* achievement; + + g_client = mock_client_logged_in(); + rc_client_set_encore_mode_enabled(g_client, 1); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501, unlock_5501_and_5502); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + /* in hardcore mode, 5501 should be unlocked, but both will appear locked due to encore mode */ + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + rc_client_destroy_achievement_list(list); + } + + rc_client_set_hardcore_enabled(g_client, 0); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + /* in softcore mode, both should be unlocked, but will appear locked due to encore mode */ + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5501); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5502); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_simple_with_unofficial_and_unsupported(void) +{ + rc_client_achievement_list_t* list; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks, no_unlocks); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5502); + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_simple_with_unofficial_off(void) +{ + rc_client_achievement_list_t* list; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 0); + mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks, no_unlocks); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 0); + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_buckets(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t** iter; + rc_client_achievement_t* achievement; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, unlock_8, unlock_8); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); /* advance achievements out of waiting state */ + event_count = 0; + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&v=732f8e30e9c1eb08948dda098c305d8b", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 6); + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + memory[5] = 5; /* trigger achievement 5 */ + memory[6] = 2; /* start measuring achievement 6 */ + memory[1] = 1; /* begin challenge achievement 7 */ + memory[0x11] = 100; /* start measuring achievements 70 and 71 */ + rc_client_do_frame(g_client); + event_count = 0; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Recently Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); + iter = list->buckets[2].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, "2/6"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 33.333333); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "25600/100000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[3].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + /* recently unlocked achievement no longer recent */ + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time -= 15 * 60; + memory[6] = 5; /* almost there achievement 6 */ + memory[1] = 0; /* stop challenge achievement 7 */ + rc_client_do_frame(g_client); + event_count = 0; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Almost There"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 6); + ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->measured_progress, "5/6"); + ASSERT_FLOAT_EQUALS(list->buckets[0].achievements[0] ->measured_percent, 83.333333); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 4); + iter = list->buckets[1].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "25600/100000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_subset_with_unofficial_and_unsupported(void) +{ + rc_client_achievement_list_t* list; + + g_client = mock_client_logged_in(); + rc_client_set_unofficial_enabled(g_client, 1); + mock_client_load_game(patchdata_unofficial_unsupported, no_unlocks, no_unlocks); + mock_client_load_subset(patchdata_subset, no_unlocks, no_unlocks); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[2].label, "Bonus - Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 3); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 7); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 8); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[2]->id, 9); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Unofficial"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5502); + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[2].label, "Sample Game - Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5503); + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[3].label, "Bonus - Locked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 3); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 7); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[1]->id, 8); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[2]->id, 9); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_subset_buckets(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t** iter; + rc_client_achievement_t* achievement; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, unlock_8, unlock_8); + mock_client_load_subset(patchdata_subset2, unlock_5502, unlock_5502); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); /* advance achievements out of waiting state */ + event_count = 0; + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&v=732f8e30e9c1eb08948dda098c305d8b", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6", + "{\"Success\":true,\"Score\":5437,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 6); + iter = list->buckets[0].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 8); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[2].label, "Multi - Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[3].label, "Multi - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + memory[5] = 5; /* trigger achievement 5 */ + memory[6] = 2; /* start measuring achievement 6 */ + memory[1] = 1; /* begin challenge achievement 7 */ + memory[0x11] = 100; /* start measuring achievements 70 and 71 */ + memory[0x17] = 7; /* trigger achievement 5501 */ + rc_client_do_frame(g_client); + event_count = 0; + + /* set the unlock time for achievement 5 back one second to ensure consistent sorting */ + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time--; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 6); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Recently Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[1]->id, 5); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[2].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); + iter = list->buckets[2].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, "2/6"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 33.333333); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "25600/100000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[3].label, "Sample Game - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); + + ASSERT_NUM_EQUALS(list->buckets[4].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[4].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[4].label, "Multi - Locked"); + ASSERT_NUM_EQUALS(list->buckets[4].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[4].achievements[0]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[5].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[5].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[5].label, "Multi - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[5].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[5].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + /* recently unlocked achievements no longer recent */ + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time -= 15 * 60; + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5501))->unlock_time -= 15 * 60; + memory[6] = 5; /* almost there achievement 6 */ + memory[1] = 0; /* stop challenge achievement 7 */ + rc_client_do_frame(g_client); + event_count = 0; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 5); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Almost There"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 6); + ASSERT_STR_EQUALS(list->buckets[0].achievements[0]->measured_progress, "5/6"); + ASSERT_FLOAT_EQUALS(list->buckets[0].achievements[0] ->measured_percent, 83.333333); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 4); + iter = list->buckets[1].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "25600/100000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, "25%"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 25.6); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[2].label, "Sample Game - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 8); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[3].label, "Multi - Locked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[4].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[4].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[4].label, "Multi - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[4].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[4].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[4].achievements[1]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_list_subset_buckets_subset_first(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t** iter; + rc_client_achievement_t* achievement; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_logged_in(); + reset_mock_api_handlers(); + mock_api_response("r=gameid&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":2345}"); + mock_api_response("r=patch&u=Username&t=ApiToken&g=2345", patchdata_subset2); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=2345&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=2345&h=0", unlock_5502); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=2345&h=1", unlock_5502); + rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); + + mock_api_response("r=patch&u=Username&t=ApiToken&g=1234", patchdata_exhaustive); + mock_api_response("r=postactivity&u=Username&t=ApiToken&a=3&m=1234&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=0", unlock_8); + mock_api_response("r=unlocks&u=Username&t=ApiToken&g=1234&h=1", unlock_8); + rc_client_begin_load_subset(g_client, 1234, rc_client_callback_expect_success, g_callback_userdata); + + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); /* advance achievements out of waiting state */ + event_count = 0; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[0].label, "Multi - Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[1].label, "Multi - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5502); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[2].label, "Sample Game - Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 6); + iter = list->buckets[2].achievements; + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 5); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 7); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 9); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + achievement = *iter++; + ASSERT_NUM_EQUALS(achievement->id, 71); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[3].label, "Sample Game - Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_achievement_get_image_url(void) +{ + char buffer[256]; + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + + ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), + RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234.png"); + + ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), + RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE, buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234_lock.png"); + + ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), + RC_CLIENT_ACHIEVEMENT_STATE_DISABLED, buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234_lock.png"); + + ASSERT_NUM_EQUALS(rc_client_achievement_get_image_url(rc_client_get_achievement_info(g_client, 5501), + RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE, buffer, sizeof(buffer)), RC_OK); + ASSERT_STR_EQUALS(buffer, "https://media.retroachievements.org/Badge/00234_lock.png"); + + rc_client_destroy(g_client); +} + +/* ----- leaderboards ----- */ + +static void test_leaderboard_list_simple(void) +{ + rc_client_leaderboard_list_t* list; + rc_client_leaderboard_t** iter; + rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, sizeof(memory)); + mock_client_load_game(patchdata_exhaustive, no_unlocks, no_unlocks); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ALL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "All"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0A] = 1; /* start 45,46,47 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ALL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "All"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_simple_with_unsupported(void) +{ + rc_client_leaderboard_list_t* list; + rc_client_leaderboard_t** iter; + rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, 0x0E); /* 0x0E address is now invalid (44,45,46,47,48)*/ + mock_client_load_game(patchdata_exhaustive, no_unlocks, no_unlocks); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ALL); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "All"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 5); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_buckets(void) +{ + rc_client_leaderboard_list_t* list; + rc_client_leaderboard_t** iter; + rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, sizeof(memory)); + mock_client_load_game(patchdata_exhaustive, no_unlocks, no_unlocks); + + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0A] = 1; /* start 45,46,47 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 3); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 4); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_buckets_with_unsupported(void) +{ + rc_client_leaderboard_list_t* list; + rc_client_leaderboard_t** iter; + rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, 0x0E); /* 0x0E address is now invalid (44,45,46,47,48)*/ + mock_client_load_game(patchdata_exhaustive, no_unlocks, no_unlocks); + + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 5); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0B] = 3; /* start 52 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 1); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 1); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Unsupported"); + ASSERT_NUM_EQUALS(list->buckets[2].num_leaderboards, 5); + + iter = list->buckets[2].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static void test_leaderboard_list_subset(void) +{ + rc_client_leaderboard_list_t* list; + rc_client_leaderboard_t** iter; + rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, sizeof(memory)); + mock_client_load_game(patchdata_exhaustive, no_unlocks, no_unlocks); + mock_client_load_subset(patchdata_subset, no_unlocks, no_unlocks); + + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Sample Game - Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 7); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[1].label, "Bonus - Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 2); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 81); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 82); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0A] = 1; /* start 45,46,47 */ + memory[0x08] = 2; /* start 82 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 4); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 45); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 82); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[1].label, "Sample Game - Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 4); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 48); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 51); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 52); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 2345); + ASSERT_STR_EQUALS(list->buckets[2].label, "Bonus - Inactive"); + ASSERT_NUM_EQUALS(list->buckets[2].num_leaderboards, 1); + + iter = list->buckets[2].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 81); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + +static const char* lbinfo_4401_top_10 = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":4401,\"GameID\":1234," + "\"LowerIsBetter\":1,\"LBTitle\":\"Leaderboard1\",\"LBDesc\":\"Desc1\",\"LBFormat\":\"SCORE\"," + "\"LBMem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"LBAuthor\":null," + "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\"," + "\"Entries\":[" + "{\"User\":\"PlayerG\",\"Score\":3524,\"Rank\":1,\"Index\":1,\"DateSubmitted\":1615654895}," + "{\"User\":\"PlayerB\",\"Score\":3645,\"Rank\":2,\"Index\":2,\"DateSubmitted\":1615634566}," + "{\"User\":\"DisplayName\",\"Score\":3754,\"Rank\":3,\"Index\":3,\"DateSubmitted\":1615234553}," + "{\"User\":\"PlayerC\",\"Score\":3811,\"Rank\":4,\"Index\":4,\"DateSubmitted\":1615653844}," + "{\"User\":\"PlayerF\",\"Score\":3811,\"Rank\":4,\"Index\":5,\"DateSubmitted\":1615623878}," + "{\"User\":\"PlayerA\",\"Score\":3811,\"Rank\":4,\"Index\":6,\"DateSubmitted\":1615653284}," + "{\"User\":\"PlayerI\",\"Score\":3902,\"Rank\":7,\"Index\":7,\"DateSubmitted\":1615632174}," + "{\"User\":\"PlayerE\",\"Score\":3956,\"Rank\":8,\"Index\":8,\"DateSubmitted\":1616384834}," + "{\"User\":\"PlayerD\",\"Score\":3985,\"Rank\":9,\"Index\":9,\"DateSubmitted\":1615238383}," + "{\"User\":\"PlayerH\",\"Score\":4012,\"Rank\":10,\"Index\":10,\"DateSubmitted\":1615638984}" + "]" + "}}"; + +static const char* lbinfo_4401_top_10_no_user = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":4401,\"GameID\":1234," + "\"LowerIsBetter\":1,\"LBTitle\":\"Leaderboard1\",\"LBDesc\":\"Desc1\",\"LBFormat\":\"SCORE\"," + "\"LBMem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"LBAuthor\":null," + "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\"," + "\"Entries\":[" + "{\"User\":\"PlayerG\",\"Score\":3524,\"Rank\":1,\"Index\":1,\"DateSubmitted\":1615654895}," + "{\"User\":\"PlayerB\",\"Score\":3645,\"Rank\":2,\"Index\":2,\"DateSubmitted\":1615634566}," + "{\"User\":\"PlayerJ\",\"Score\":3754,\"Rank\":3,\"Index\":3,\"DateSubmitted\":1615234553}," + "{\"User\":\"PlayerC\",\"Score\":3811,\"Rank\":4,\"Index\":4,\"DateSubmitted\":1615653844}," + "{\"User\":\"PlayerF\",\"Score\":3811,\"Rank\":4,\"Index\":5,\"DateSubmitted\":1615623878}," + "{\"User\":\"PlayerA\",\"Score\":3811,\"Rank\":4,\"Index\":6,\"DateSubmitted\":1615653284}," + "{\"User\":\"PlayerI\",\"Score\":3902,\"Rank\":7,\"Index\":7,\"DateSubmitted\":1615632174}," + "{\"User\":\"PlayerE\",\"Score\":3956,\"Rank\":8,\"Index\":8,\"DateSubmitted\":1616384834}," + "{\"User\":\"PlayerD\",\"Score\":3985,\"Rank\":9,\"Index\":9,\"DateSubmitted\":1615238383}," + "{\"User\":\"PlayerH\",\"Score\":4012,\"Rank\":10,\"Index\":10,\"DateSubmitted\":1615638984}" + "]" + "}}"; + +static const char* lbinfo_4401_near_user = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":4401,\"GameID\":1234," + "\"LowerIsBetter\":1,\"LBTitle\":\"Leaderboard1\",\"LBDesc\":\"Desc1\",\"LBFormat\":\"SCORE\"," + "\"LBMem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"LBAuthor\":null," + "\"LBCreated\":\"2013-10-20 22:12:21\",\"LBUpdated\":\"2021-06-14 08:18:19\"," + "\"Entries\":[" + "{\"User\":\"PlayerG\",\"Score\":3524,\"Rank\":17,\"Index\":17,\"DateSubmitted\":1615654895}," + "{\"User\":\"PlayerB\",\"Score\":3645,\"Rank\":18,\"Index\":18,\"DateSubmitted\":1615634566}," + "{\"User\":\"PlayerC\",\"Score\":3811,\"Rank\":19,\"Index\":19,\"DateSubmitted\":1615653844}," + "{\"User\":\"PlayerF\",\"Score\":3811,\"Rank\":19,\"Index\":20,\"DateSubmitted\":1615623878}," + "{\"User\":\"DisplayName\",\"Score\":3811,\"Rank\":19,\"Index\":21,\"DateSubmitted\":1615234553}," + "{\"User\":\"PlayerA\",\"Score\":3811,\"Rank\":19,\"Index\":22,\"DateSubmitted\":1615653284}," + "{\"User\":\"PlayerI\",\"Score\":3902,\"Rank\":23,\"Index\":23,\"DateSubmitted\":1615632174}," + "{\"User\":\"PlayerE\",\"Score\":3956,\"Rank\":24,\"Index\":24,\"DateSubmitted\":1616384834}," + "{\"User\":\"PlayerD\",\"Score\":3985,\"Rank\":25,\"Index\":25,\"DateSubmitted\":1615238383}," + "{\"User\":\"PlayerH\",\"Score\":4012,\"Rank\":26,\"Index\":26,\"DateSubmitted\":1615638984}" + "]" + "}}"; + +static rc_client_leaderboard_entry_list_t* g_leaderboard_entries = NULL; +static void rc_client_callback_expect_leaderboard_entry_list(int result, const char* error_message, rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_OK); + ASSERT_PTR_NULL(error_message); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); + + ASSERT_PTR_NOT_NULL(list); + g_leaderboard_entries = list; +} + +static void test_fetch_leaderboard_entries(void) +{ + rc_client_leaderboard_entry_t* entry; + char url[256]; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + g_leaderboard_entries = NULL; + + mock_api_response("r=lbinfo&i=4401&c=10", lbinfo_4401_top_10); + + rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, + rc_client_callback_expect_leaderboard_entry_list, g_callback_userdata); + ASSERT_PTR_NOT_NULL(g_leaderboard_entries); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->num_entries, 10); + + entry = g_leaderboard_entries->entries; + ASSERT_STR_EQUALS(entry->user, "PlayerG"); + ASSERT_STR_EQUALS(entry->display, "003524"); + ASSERT_NUM_EQUALS(entry->index, 1); + ASSERT_NUM_EQUALS(entry->rank, 1); + ASSERT_NUM_EQUALS(entry->submitted, 1615654895); + + ASSERT_NUM_EQUALS(rc_client_leaderboard_entry_get_user_image_url(entry, url, sizeof(url)), RC_OK); + ASSERT_STR_EQUALS(url, "https://media.retroachievements.org/UserPic/PlayerG.png"); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerB"); + ASSERT_STR_EQUALS(entry->display, "003645"); + ASSERT_NUM_EQUALS(entry->index, 2); + ASSERT_NUM_EQUALS(entry->rank, 2); + ASSERT_NUM_EQUALS(entry->submitted, 1615634566); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "DisplayName"); + ASSERT_STR_EQUALS(entry->display, "003754"); + ASSERT_NUM_EQUALS(entry->index, 3); + ASSERT_NUM_EQUALS(entry->rank, 3); + ASSERT_NUM_EQUALS(entry->submitted, 1615234553); + + ASSERT_NUM_EQUALS(rc_client_leaderboard_entry_get_user_image_url(entry, url, sizeof(url)), RC_OK); + ASSERT_STR_EQUALS(url, "https://media.retroachievements.org/UserPic/DisplayName.png"); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerC"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 4); + ASSERT_NUM_EQUALS(entry->rank, 4); + ASSERT_NUM_EQUALS(entry->submitted, 1615653844); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerF"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 5); + ASSERT_NUM_EQUALS(entry->rank, 4); + ASSERT_NUM_EQUALS(entry->submitted, 1615623878); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerA"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 6); + ASSERT_NUM_EQUALS(entry->rank, 4); + ASSERT_NUM_EQUALS(entry->submitted, 1615653284); + + ASSERT_NUM_EQUALS(rc_client_leaderboard_entry_get_user_image_url(entry, url, sizeof(url)), RC_OK); + ASSERT_STR_EQUALS(url, "https://media.retroachievements.org/UserPic/PlayerA.png"); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerI"); + ASSERT_STR_EQUALS(entry->display, "003902"); + ASSERT_NUM_EQUALS(entry->index, 7); + ASSERT_NUM_EQUALS(entry->rank, 7); + ASSERT_NUM_EQUALS(entry->submitted, 1615632174); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerE"); + ASSERT_STR_EQUALS(entry->display, "003956"); + ASSERT_NUM_EQUALS(entry->index, 8); + ASSERT_NUM_EQUALS(entry->rank, 8); + ASSERT_NUM_EQUALS(entry->submitted, 1616384834); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerD"); + ASSERT_STR_EQUALS(entry->display, "003985"); + ASSERT_NUM_EQUALS(entry->index, 9); + ASSERT_NUM_EQUALS(entry->rank, 9); + ASSERT_NUM_EQUALS(entry->submitted, 1615238383); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerH"); + ASSERT_STR_EQUALS(entry->display, "004012"); + ASSERT_NUM_EQUALS(entry->index, 10); + ASSERT_NUM_EQUALS(entry->rank, 10); + ASSERT_NUM_EQUALS(entry->submitted, 1615638984); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->user_index, 2); + + rc_client_destroy_leaderboard_entry_list(g_leaderboard_entries); + rc_client_destroy(g_client); +} + +static void test_fetch_leaderboard_entries_no_user(void) +{ + rc_client_leaderboard_entry_t* entry; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + g_leaderboard_entries = NULL; + + mock_api_response("r=lbinfo&i=4401&c=10", lbinfo_4401_top_10_no_user); + + rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, + rc_client_callback_expect_leaderboard_entry_list, g_callback_userdata); + ASSERT_PTR_NOT_NULL(g_leaderboard_entries); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->num_entries, 10); + + entry = g_leaderboard_entries->entries; + ASSERT_STR_EQUALS(entry->user, "PlayerG"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerB"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerJ"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerC"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerF"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerA"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerI"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerE"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerD"); + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerH"); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->user_index, -1); + + rc_client_destroy_leaderboard_entry_list(g_leaderboard_entries); + rc_client_destroy(g_client); +} + +static void test_fetch_leaderboard_entries_around_user(void) +{ + rc_client_leaderboard_entry_t* entry; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + g_leaderboard_entries = NULL; + + mock_api_response("r=lbinfo&i=4401&u=Username&c=10", lbinfo_4401_near_user); + + rc_client_begin_fetch_leaderboard_entries_around_user(g_client, 4401, 10, + rc_client_callback_expect_leaderboard_entry_list, g_callback_userdata); + ASSERT_PTR_NOT_NULL(g_leaderboard_entries); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->num_entries, 10); + + entry = g_leaderboard_entries->entries; + ASSERT_STR_EQUALS(entry->user, "PlayerG"); + ASSERT_STR_EQUALS(entry->display, "003524"); + ASSERT_NUM_EQUALS(entry->index, 17); + ASSERT_NUM_EQUALS(entry->rank, 17); + ASSERT_NUM_EQUALS(entry->submitted, 1615654895); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerB"); + ASSERT_STR_EQUALS(entry->display, "003645"); + ASSERT_NUM_EQUALS(entry->index, 18); + ASSERT_NUM_EQUALS(entry->rank, 18); + ASSERT_NUM_EQUALS(entry->submitted, 1615634566); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerC"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 19); + ASSERT_NUM_EQUALS(entry->rank, 19); + ASSERT_NUM_EQUALS(entry->submitted, 1615653844); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerF"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 20); + ASSERT_NUM_EQUALS(entry->rank, 19); + ASSERT_NUM_EQUALS(entry->submitted, 1615623878); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "DisplayName"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 21); + ASSERT_NUM_EQUALS(entry->rank, 19); + ASSERT_NUM_EQUALS(entry->submitted, 1615234553); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerA"); + ASSERT_STR_EQUALS(entry->display, "003811"); + ASSERT_NUM_EQUALS(entry->index, 22); + ASSERT_NUM_EQUALS(entry->rank, 19); + ASSERT_NUM_EQUALS(entry->submitted, 1615653284); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerI"); + ASSERT_STR_EQUALS(entry->display, "003902"); + ASSERT_NUM_EQUALS(entry->index, 23); + ASSERT_NUM_EQUALS(entry->rank, 23); + ASSERT_NUM_EQUALS(entry->submitted, 1615632174); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerE"); + ASSERT_STR_EQUALS(entry->display, "003956"); + ASSERT_NUM_EQUALS(entry->index, 24); + ASSERT_NUM_EQUALS(entry->rank, 24); + ASSERT_NUM_EQUALS(entry->submitted, 1616384834); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerD"); + ASSERT_STR_EQUALS(entry->display, "003985"); + ASSERT_NUM_EQUALS(entry->index, 25); + ASSERT_NUM_EQUALS(entry->rank, 25); + ASSERT_NUM_EQUALS(entry->submitted, 1615238383); + + ++entry; + ASSERT_STR_EQUALS(entry->user, "PlayerH"); + ASSERT_STR_EQUALS(entry->display, "004012"); + ASSERT_NUM_EQUALS(entry->index, 26); + ASSERT_NUM_EQUALS(entry->rank, 26); + ASSERT_NUM_EQUALS(entry->submitted, 1615638984); + + ASSERT_NUM_EQUALS(g_leaderboard_entries->user_index, 4); + + rc_client_destroy_leaderboard_entry_list(g_leaderboard_entries); + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_leaderboard_entry_list_login_required(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) +{ + ASSERT_NUM_EQUALS(result, RC_LOGIN_REQUIRED); + ASSERT_STR_EQUALS(error_message, "Login required"); + ASSERT_PTR_EQUALS(client, g_client); + ASSERT_PTR_EQUALS(callback_userdata, g_callback_userdata); + ASSERT_PTR_NULL(list); +} + +static void test_fetch_leaderboard_entries_around_user_not_logged_in(void) +{ + g_client = mock_client_not_logged_in(); + g_leaderboard_entries = NULL; + + mock_api_response("r=lbinfo&i=4401&u=Username&c=10", lbinfo_4401_near_user); + + rc_client_begin_fetch_leaderboard_entries_around_user(g_client, 4401, 10, + rc_client_callback_expect_leaderboard_entry_list_login_required, g_callback_userdata); + ASSERT_PTR_NULL(g_leaderboard_entries); + + assert_api_not_called("r=lbinfo&i=4401&u=Username&c=10"); + + rc_client_destroy(g_client); +} + +static void rc_client_callback_expect_leaderboard_uncalled(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, rc_client_t* client, void* callback_userdata) +{ + ASSERT_FAIL("Callback should not have been called.") +} + +static void test_fetch_leaderboard_entries_aborted(void) +{ + rc_client_async_handle_t* handle; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + g_leaderboard_entries = NULL; + + handle = rc_client_begin_fetch_leaderboard_entries(g_client, 4401, 1, 10, + rc_client_callback_expect_leaderboard_uncalled, g_callback_userdata); + + rc_client_abort_async(g_client, handle); + + async_api_response("r=lbinfo&i=4401&c=10", lbinfo_4401_top_10); + ASSERT_PTR_NULL(g_leaderboard_entries); + + rc_client_destroy(g_client); +} + +/* ----- do frame ----- */ + +static void test_do_frame_bounds_check_system(void) +{ + g_client = mock_client_game_loaded(patchdata_bounds_check_system, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + assert_achievement_state(g_client, 1, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 2, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 3, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* 0x10000 out of range for system */ + assert_achievement_state(g_client, 4, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 5, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); /* cannot read two bytes from 0xFFFF, but size isn't enforced until do_frame */ + assert_achievement_state(g_client, 6, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* 0x10000 out of range for system */ + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_bounds_check_available(void) +{ + uint8_t memory[8] = { 0,0,0,0,0,0,0,0 }; + g_client = mock_client_game_loaded(patchdata_bounds_check_8, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + /* all addresses are valid according to the system, so no achievements should be disabled yet. */ + assert_achievement_state(g_client, 808, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + + /* limit the memory that's actually exposed and try to process a frame */ + mock_memory(memory, sizeof(memory)); + rc_client_do_frame(g_client); + + assert_achievement_state(g_client, 408, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 508, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 608, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 708, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 808, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ + + assert_achievement_state(g_client, 416, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 516, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 616, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 716, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only one byte available */ + assert_achievement_state(g_client, 816, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ + + assert_achievement_state(g_client, 424, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 524, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* 24-bit read actually fetches 32-bits */ + assert_achievement_state(g_client, 624, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only two bytes available */ + assert_achievement_state(g_client, 724, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only one byte available */ + assert_achievement_state(g_client, 824, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ + + assert_achievement_state(g_client, 432, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + assert_achievement_state(g_client, 532, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only three bytes available */ + assert_achievement_state(g_client, 632, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only two bytes available */ + assert_achievement_state(g_client, 732, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* only one byte available */ + assert_achievement_state(g_client, 832, RC_CLIENT_ACHIEVEMENT_STATE_DISABLED); /* out of bounds*/ + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_already_awarded(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_server_error(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":false,\"Error\":\"Achievement not found\"}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + /* achievement still counts as triggered */ + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12345 + 5); /* score will have been adjusted locally, but not from server */ + + /* but an error should have been reported */ + event = find_event(RC_CLIENT_EVENT_SERVER_ERROR, 0); + ASSERT_PTR_NOT_NULL(event); + ASSERT_STR_EQUALS(event->server_error->api, "award_achievement"); + ASSERT_STR_EQUALS(event->server_error->error_message, "Achievement not found"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_while_spectating(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); + rc_client_set_spectator_mode_enabled(g_client, 1); + ASSERT_TRUE(rc_client_get_spectator_mode_enabled(g_client)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":false,\"Error\":\"Achievement should not have been unlocked in spectating mode\"}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + /* achievement still counts as triggered */ + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12345 + 5); /* score will have been adjusted locally, but not from server */ + + /* expect API not called */ + assert_api_not_called("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_client_set_spectator_mode_enabled(g_client, 0); + ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_automatic_retry(void) +{ + const char* unlock_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6"; + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + /* discard the queued ping to make finding the retry easier */ + g_client->state.scheduled_callbacks = NULL; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 3; + memory[2] = 7; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 5501)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* first failure will immediately requeue the request */ + async_api_response(unlock_request_params, ""); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* second failure will queue it */ + async_api_response(unlock_request_params, ""); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + g_client->state.scheduled_callbacks->when = 0; + rc_client_idle(g_client); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* third failure will requeue it */ + async_api_response(unlock_request_params, ""); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + g_client->state.scheduled_callbacks->when = 0; + rc_client_idle(g_client); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* success should not requeue it and update player score */ + async_api_response(unlock_request_params, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_trigger_subset(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + mock_client_load_subset(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6", + "{\"Success\":true,\"Score\":5437,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + memory[1] = 3; + memory[2] = 7; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 5501); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 5501)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + ASSERT_NUM_EQUALS(g_client->user.score, 5437); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + } + + rc_client_destroy(g_client); +} + + +static void test_do_frame_achievement_measured(void) +{ + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=70&h=1&m=0123456789ABCDEF&v=61e40027573e2cde88b49d27f6804879", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":70,\"AchievementsRemaining\":11}"); + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=71&h=1&m=0123456789ABCDEF&v=3a8d55b81d391557d5111306599a2b0d", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":71,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[0x10] = 0x39; memory[0x11] = 0x30; /* 12345 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); /* one PROGRESS_INDICATOR_SHOW event */ + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "12345/100000"); + + achievement = rc_client_get_achievement_info(g_client, 71); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "12%"); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increment measured value - raw counter will report progress change, percentage will not */ + memory[0x10] = 0x3A; /* 12346 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); /* one PROGRESS_INDICATOR_SHOW event */ + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "12346/100000"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* increment measured value - raw counter will report progress change, percentage will not */ + memory[0x11] = 0x33; /* 13114 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); /* one PROGRESS_INDICATOR_SHOW event */ + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "13114/100000"); + + achievement = rc_client_get_achievement_info(g_client, 71); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, "13%"); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* trigger measured achievements - progress becomes blank */ + memory[0x10] = 0xA0; memory[0x11] = 0x86; memory[0x12] = 0x01; /* 100000 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); /* two TRIGGERED events, and no PROGRESS_INDICATOR_SHOW events */ + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + + achievement = rc_client_get_achievement_info(g_client, 71); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_measured_progress_event(void) +{ + rc_client_event_t* event; + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=6&h=1&m=0123456789ABCDEF&v=65206f4290098ecd30c7845e895057d0", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":6,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[0x06] = 3; /* 3/6 */ + memory[0x11] = 0xC3; memory[0x10] = 0x4F; /* 49999/100000 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + /* 3/6 = 50%, 49999/100000 = 49.999% */ + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 6)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, "3/6"); + + /* both achievements should have been updated, */ + achievement = rc_client_get_achievement_info(g_client, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, "3/6"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 50.0); + + achievement = rc_client_get_achievement_info(g_client, 70); + ASSERT_STR_EQUALS(achievement->measured_progress, "49999/100000"); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 49.999); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* any change will trigger the popup - even dropping */ + memory[0x10] = 0x4E; /* 49998 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 70); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 70)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, "49998/100000"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* don't trigger popup when value changes to 0 as the measured_progress string will be blank */ + memory[0x06] = 0; /* 0 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 6); + ASSERT_STR_EQUALS(achievement->measured_progress, ""); + ASSERT_FLOAT_EQUALS(achievement->measured_percent, 0.0); + + /* both at 50%, only report first */ + memory[0x06] = 3; /* 3/6 */ + memory[0x11] = 0xC3; memory[0x10] = 0x50; /* 50000/100000 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 6); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 6)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, "3/6"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* second slightly ahead */ + memory[0x6] = 4; /* 4/6 */ + memory[0x12] = 1; memory[0x11] = 0x04; memory[0x10] = 0x6B; /* 66667/100000 */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW, 70); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 70)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, "66667/100000"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* don't show popup on trigger */ + memory[0x06] = 6; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 6); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 6)); + ASSERT_STR_EQUALS(event->achievement->measured_progress, ""); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_achievement_challenge_indicator(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=7&h=1&m=0123456789ABCDEF&v=c39308ba325ba4a72919b081fb18fdd4", + "{\"Success\":true,\"Score\":5432,\"AchievementID\":7,\"AchievementsRemaining\":11}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 1; /* show indicator */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 0; /* hide indicator */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[1] = 1; /* show indicator */ + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE); + ASSERT_NUM_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* trigger achievement - expect both hide and trigger events. both should have triggered achievement data */ + memory[7] = 7; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 7); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 7)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_mastery(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12345+5); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 0); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + async_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":0}"); + + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_GAME_COMPLETED, 1234); + ASSERT_PTR_NOT_NULL(event); + + memory[9] = 9; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 9); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 9)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + ASSERT_NUM_EQUALS(g_client->user.score, 5432+5); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + async_api_response("r=awardachievement&u=Username&t=ApiToken&a=9&h=1&m=0123456789ABCDEF&v=6d989ee0f408660a87d6440a13563bf6", + "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":9,\"AchievementsRemaining\":0}"); + + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_mastery_encore(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + const uint32_t num_active = g_client->game->runtime.trigger_count; + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[8] = 8; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 8); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 8)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 1); + ASSERT_NUM_EQUALS(g_client->user.score, 12345+5); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 0); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + async_api_response("r=awardachievement&u=Username&t=ApiToken&a=8&h=1&m=0123456789ABCDEF&v=da80b659c2b858e13ddd97077647b217", + "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":0}"); + + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_GAME_COMPLETED, 1234); + ASSERT_PTR_NOT_NULL(event); + + memory[9] = 9; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED, 9); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(event->achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_NOT_EQUALS(event->achievement->unlock_time, 0); + ASSERT_NUM_EQUALS(event->achievement->bucket, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_PTR_EQUALS(event->achievement, rc_client_get_achievement_info(g_client, 9)); + + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, num_active - 2); + ASSERT_NUM_EQUALS(g_client->user.score, 5432+5); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + async_api_response("r=awardachievement&u=Username&t=ApiToken&a=9&h=1&m=0123456789ABCDEF&v=6d989ee0f408660a87d6440a13563bf6", + "{\"Success\":false,\"Error\":\"User already has hardcore and regular achievements awarded.\",\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":9,\"AchievementsRemaining\":0}"); + + ASSERT_NUM_EQUALS(event_count, 0); + ASSERT_NUM_EQUALS(g_client->user.score, 5432); + ASSERT_NUM_EQUALS(g_client->user.score_softcore, 777); + + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_started(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_update(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* update the leaderboard */ + memory[0x0E] = 18; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000018"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_failed(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel the leaderboard */ + memory[0x0C] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_server_error(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", + "{\"Success\":false,\"Error\":\"Leaderboard not found\"}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + /* an error should have also been reported */ + event = find_event(RC_CLIENT_EVENT_SERVER_ERROR, 0); + ASSERT_PTR_NOT_NULL(event); + ASSERT_STR_EQUALS(event->server_error->api, "submit_lboard_entry"); + ASSERT_STR_EQUALS(event->server_error->error_message, "Leaderboard not found"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_while_spectating(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + ASSERT_FALSE(rc_client_get_spectator_mode_enabled(g_client)); + rc_client_set_spectator_mode_enabled(g_client, 1); + ASSERT_TRUE(rc_client_get_spectator_mode_enabled(g_client)); + + mock_api_response("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6", + "{\"Success\":false,\"Error\":\"Leaderboard entry should not have been submitted in spectating mode\"}"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* expect API not called */ + assert_api_not_called("r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6"); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_tracker_sharing(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start one leaderboard (one tracker) */ + memory[0x0B] = 1; + memory[0x0E] = 17; + memory[0x0F] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000273"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start additional leaderboards (45,46,47) - 45 and 46 should generate new trackers */ + memory[0x0A] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 5); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 45); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 45)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 1); /* 45 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 46); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 46)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 1); /* 46 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 47); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 47)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 2); /* 44,47 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 2); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); /* 45 has different size */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 3); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 3); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "273"); /* 46 has different format */ + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start additional leaderboard (48) - should share tracker with 44 */ + memory[0x0A] = 2; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 48); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 48)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 3); /* 44,47,48 */ + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel leaderboard 44 */ + memory[0x0C] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 2); /* 47,48 */ + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel leaderboard 45 */ + memory[0x0C] = 2; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 45); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 45)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 0); /* */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 2); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel leaderboard 46 */ + memory[0x0C] = 3; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 46); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 46)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 0); /* */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 3); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 3); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "273"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel 47, start 51 */ + memory[0x0A] = 3; + memory[0x0B] = 0; + memory[0x0C] = 4; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 47); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 47)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 1); /* 48 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 51); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "0"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->next->reference_count, 1); /* 51 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 2); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "0"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* cancel 48 */ + memory[0x0C] = 5; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 48); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000273"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 48)); + ASSERT_NUM_EQUALS(g_client->game->leaderboard_trackers->reference_count, 0); /* */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000273"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_tracker_sharing_hits(void) +{ + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start leaderboards 51,52 (share tracker) */ + memory[0x0A] = 3; + memory[0x0B] = 3; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 51); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "0"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 52); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "0"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 52)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "0"); + + /* hit count ticks */ + memory[0x09] = 1; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "1"); + + /* cancel leaderboard 51 */ + memory[0x0C] = 6; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_FAILED, 51); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "2"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "2"); + + /* hit count ticks */ + memory[0x0A] = 0; + memory[0x0C] = 0; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "3"); + + /* restart leaderboard 51 - hit count differs, can't share */ + memory[0x0A] = 3; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 3); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 51); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "1"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 51)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "4"); /* 52 */ + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 2); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 2); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "1"); /* 51 */ + } + + rc_client_destroy(g_client); +} + +static void test_do_frame_leaderboard_submit_automatic_retry(void) +{ + const char* submit_entry_params = "r=submitlbentry&u=Username&t=ApiToken&i=44&s=17&m=0123456789ABCDEF&v=a27fa205f7f30c8d13d74806ea5425b6"; + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + g_client->callbacks.server_call = rc_client_server_call_async; + + /* discard the queued ping to make finding the retry easier */ + g_client->state.scheduled_callbacks = NULL; + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + mock_memory(memory, sizeof(memory)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* start the leaderboard */ + memory[0x0B] = 1; + memory[0x0E] = 17; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 44)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* submit the leaderboard */ + memory[0x0D] = 1; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED, 44); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_STR_EQUALS(event->leaderboard->tracker_value, "000017"); + ASSERT_PTR_EQUALS(event->leaderboard, rc_client_get_leaderboard_info(g_client, 44)); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000017"); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + /* first failure will immediately requeue the request */ + async_api_response(submit_entry_params, ""); + assert_api_pending(submit_entry_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* second failure will queue it */ + async_api_response(submit_entry_params, ""); + assert_api_call_count(submit_entry_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + g_client->state.scheduled_callbacks->when = 0; + rc_client_idle(g_client); + assert_api_pending(submit_entry_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* third failure will requeue it */ + async_api_response(submit_entry_params, ""); + assert_api_call_count(submit_entry_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + + g_client->state.scheduled_callbacks->when = 0; + rc_client_idle(g_client); + assert_api_pending(submit_entry_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + /* success should not requeue it and update player score */ + async_api_response(submit_entry_params, + "{\"Success\":true,\"Response\":{\"Score\":17,\"BestScore\":23," + "\"TopEntries\":[{\"User\":\"Player1\",\"Score\":44,\"Rank\":1},{\"User\":\"Username\",\"Score\":23,\"Rank\":2}]," + "\"RankInfo\":{\"Rank\":2,\"NumEntries\":\"2\"}}}"); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + } + + rc_client_destroy(g_client); +} + +/* ----- ping ----- */ + +static void test_idle_ping(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + rc_client_scheduled_callback_t ping_callback; + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + g_client->state.scheduled_callbacks->when = 0; + ping_callback = g_client->state.scheduled_callbacks->callback; + + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234", "{\"Success\":true}"); + + rc_client_idle(g_client); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_NUM_GREATER(g_client->state.scheduled_callbacks->when, time(NULL) + 100); + ASSERT_NUM_LESS(g_client->state.scheduled_callbacks->when, time(NULL) + 150); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + } + + /* unloading game should unschedule ping */ + rc_client_unload_game(g_client); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + + rc_client_destroy(g_client); +} + +static void test_do_frame_ping_rich_presence(void) +{ + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + + ASSERT_PTR_NOT_NULL(g_client->game); + if (g_client->game) { + rc_client_scheduled_callback_t ping_callback; + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + g_client->state.scheduled_callbacks->when = 0; + ping_callback = g_client->state.scheduled_callbacks->callback; + + mock_memory(memory, sizeof(memory)); + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a0", "{\"Success\":true}"); + + rc_client_do_frame(g_client); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_NUM_GREATER(g_client->state.scheduled_callbacks->when, time(NULL) + 100); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + + g_client->state.scheduled_callbacks->when = 0; + mock_api_response("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25", "{\"Success\":true}"); + memory[0x03] = 25; + + rc_client_do_frame(g_client); + + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + ASSERT_NUM_GREATER(g_client->state.scheduled_callbacks->when, time(NULL) + 100); + ASSERT_PTR_EQUALS(g_client->state.scheduled_callbacks->callback, ping_callback); + + assert_api_called("r=ping&u=Username&t=ApiToken&g=1234&m=Points%3a25"); + } + + rc_client_destroy(g_client); +} + +static void test_reset_hides_widgets(void) +{ + const rc_client_leaderboard_t* leaderboard; + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + memory[0x01] = 1; /* challenge indicator for achievement 7 */ + memory[0x0A] = 2; /* tracker for leaderboard 48 */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + rc_client_reset(g_client); + + ASSERT_NUM_EQUALS(event_count, 2); /* challenge indicator hide, tracker hide */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); + + /* non tracked achievements/leaderboards should also be reset to waiting */ + achievement = rc_client_get_achievement_info(g_client, 5); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); + + leaderboard = rc_client_get_leaderboard_info(g_client, 46); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); + + rc_client_destroy(g_client); +} + +/* ----- progress ----- */ + +static void test_deserialize_progress_updates_widgets(void) +{ + const rc_client_leaderboard_t* leaderboard; + const rc_client_achievement_t* achievement; + const rc_client_event_t* event; + uint8_t* serialized1; + uint8_t* serialized2; + size_t serialize_size; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + /* create an initial checkpoint */ + serialize_size = rc_client_progress_size(g_client); + serialized1 = (uint8_t*)malloc(serialize_size); + serialized2 = (uint8_t*)malloc(serialize_size); + ASSERT_NUM_EQUALS(rc_client_serialize_progress(g_client, serialized1), RC_OK); + + /* activate some widgets */ + memory[0x01] = 1; /* challenge indicator for achievement 7 */ + memory[0x0A] = 2; /* tracker for leaderboard 48 */ + memory[0x0E] = 25; /* leaderboard 48 value */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* capture the state with the widgets visible */ + ASSERT_NUM_EQUALS(rc_client_serialize_progress(g_client, serialized2), RC_OK); + + /* deserialize current state. expect no changes */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized2), RC_OK); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* deserialize original state. expect challenge indicator hide, tracker hide */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized1), RC_OK); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 0); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_ACTIVE); + + /* deserialize second state. expect challenge indicator show, tracker show */ + event_count = 0; + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized2), RC_OK); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* update tracker value */ + memory[0x0E] = 30; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 1); + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000030"); + + /* deserialize second state. expect challenge tracker update to old value */ + event_count = 0; + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, serialized2), RC_OK); + ASSERT_NUM_EQUALS(event_count, 1); + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000025"); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + free(serialized2); + free(serialized1); + rc_client_destroy(g_client); +} + +static void test_deserialize_progress_null(void) +{ + const rc_client_leaderboard_t* leaderboard; + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + /* activate some widgets */ + memory[0x01] = 1; /* challenge indicator for achievement 7 */ + memory[0x0A] = 2; /* tracker for leaderboard 48 */ + memory[0x0E] = 25; /* leaderboard 48 value */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* deserialize null state. expect all widgets to be hidden and achievements reset to waiting */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, NULL), RC_OK); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 0); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); + + /* must be false before it can be true to change from WAITING to ACTIVE. do so manually */ + ((rc_client_leaderboard_info_t*)leaderboard)->lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* advance frame, challenge indicator and leaderboard tracker should reappear */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + rc_client_destroy(g_client); +} + +static void test_deserialize_progress_invalid(void) +{ + const rc_client_leaderboard_t* leaderboard; + const rc_client_achievement_t* achievement; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, no_unlocks, no_unlocks); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + /* activate some widgets */ + memory[0x01] = 1; /* challenge indicator for achievement 7 */ + memory[0x0A] = 2; /* tracker for leaderboard 48 */ + memory[0x0E] = 25; /* leaderboard 48 value */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 0); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_PRIMED); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 2); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_STARTED); + + /* deserialize null state. expect all widgets to be hidden and achievements reset to waiting */ + ASSERT_NUM_EQUALS(rc_client_deserialize_progress(g_client, memory), RC_INVALID_STATE); + ASSERT_NUM_EQUALS(event_count, 2); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1)); + + achievement = rc_client_get_achievement_info(g_client, 7); + ASSERT_PTR_NOT_NULL(achievement); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->state, RC_TRIGGER_STATE_WAITING); + ASSERT_NUM_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger->requirement->conditions->next->current_hits, 0); + + leaderboard = rc_client_get_leaderboard_info(g_client, 48); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(((rc_client_leaderboard_info_t*)leaderboard)->lboard->state, RC_LBOARD_STATE_WAITING); + + /* must be false before it can be true to change from WAITING to ACTIVE. do so manually */ + ((rc_client_leaderboard_info_t*)leaderboard)->lboard->state = RC_LBOARD_STATE_ACTIVE; + + /* advance frame, challenge indicator and leaderboard tracker should reappear */ + event_count = 0; + rc_client_do_frame(g_client); + + ASSERT_NUM_EQUALS(event_count, 3); /* challenge indicator show, leaderboard start, tracker show */ + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW, 7)); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1)); + + rc_client_destroy(g_client); +} + +/* ----- processing required ----- */ + +static void test_processing_required(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501, unlock_5501_and_5502); + + ASSERT_TRUE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_empty_game(void) +{ + g_client = mock_client_game_loaded(patchdata_empty, no_unlocks, no_unlocks); + + ASSERT_FALSE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_rich_presence_only(void) +{ + g_client = mock_client_game_loaded(patchdata_rich_presence_only, no_unlocks, no_unlocks); + + ASSERT_TRUE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_leaderboard_only(void) +{ + g_client = mock_client_game_loaded(patchdata_leaderboard_only, no_unlocks, no_unlocks); + + ASSERT_TRUE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_after_mastery(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501_and_5502, unlock_5501_and_5502); + + ASSERT_TRUE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +static void test_processing_required_after_mastery_no_leaderboards(void) +{ + g_client = mock_client_game_loaded(patchdata_2ach_0lbd, unlock_5501_and_5502, unlock_5501_and_5502); + + ASSERT_FALSE(rc_client_is_processing_required(g_client)); + + rc_client_destroy(g_client); +} + +/* ----- settings ----- */ + +static void test_set_hardcore_disable(void) +{ + const rc_client_achievement_t* achievement; + const rc_client_leaderboard_t* leaderboard; + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501, unlock_5501_and_5502); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); /* 5502 should be active*/ + } + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + if (leaderboard) { + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 1); + } + + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); + + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 0); /* 5502 should not be active*/ + } + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + if (leaderboard) { + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_INACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 0); + } + + rc_client_destroy(g_client); +} + +static void test_set_hardcore_disable_active_tracker(void) +{ + const rc_client_leaderboard_t* leaderboard; + rc_client_event_t* event; + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_2ach_1lbd, unlock_5501, unlock_5501_and_5502); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); + + memory[0x0C] = 1; + memory[0x0E] = 25; + event_count = 0; + rc_client_do_frame(g_client); + ASSERT_NUM_EQUALS(event_count, 2); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_STARTED, 4401); + ASSERT_PTR_NOT_NULL(event); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + ASSERT_STR_EQUALS(event->leaderboard_tracker->display, "000025"); + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_TRACKING); + + event_count = 0; + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); + ASSERT_NUM_EQUALS(event_count, 1); + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_INACTIVE); + + event = find_event(RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE, 1); + ASSERT_PTR_NOT_NULL(event); + ASSERT_NUM_EQUALS(event->leaderboard_tracker->id, 1); + + rc_client_destroy(g_client); +} + +static void test_set_hardcore_enable(void) +{ + const rc_client_achievement_t* achievement; + const rc_client_leaderboard_t* leaderboard; + + g_client = mock_client_logged_in(); + rc_client_set_hardcore_enabled(g_client, 0); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501, unlock_5501_and_5502); + + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 0); /* 5502 should not be active*/ + } + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + if (leaderboard) { + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_INACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 0); + } + + /* when enabling hardcore, flag waiting_for_reset. this will prevent processing until rc_client_reset is called */ + event_count = 0; + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 1); + ASSERT_PTR_NOT_NULL(find_event(RC_CLIENT_EVENT_RESET, 0)); + + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); /* 5502 should be active*/ + } + + leaderboard = rc_client_get_leaderboard_info(g_client, 4401); + ASSERT_PTR_NOT_NULL(leaderboard); + if (leaderboard) { + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.lboard_count, 1); + } + + /* resetting clears waiting_for_reset */ + rc_client_reset(g_client); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); + + /* hardcore already enabled, attempting to set it again shouldn't flag waiting_for_reset */ + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->waiting_for_reset, 0); + + rc_client_destroy(g_client); +} + +static void test_set_hardcore_enable_no_game_loaded(void) +{ + g_client = mock_client_logged_in(); + rc_client_set_hardcore_enabled(g_client, 0); + + /* enabling hardcore before a game is loaded just toggles the flag */ + event_count = 0; + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(event_count, 0); + + rc_client_destroy(g_client); +} + +static void test_set_hardcore_enable_encore_mode(void) +{ + const rc_client_achievement_t* achievement; + rc_client_achievement_info_t* achievement_info; + + g_client = mock_client_logged_in(); + rc_client_set_encore_mode_enabled(g_client, 1); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501, unlock_5501_and_5502); + + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 2); + + g_client->game->runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_ACTIVE; + g_client->game->runtime.triggers[1].trigger->state = RC_TRIGGER_STATE_ACTIVE; + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); /* unlock information still tracked */ + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); /* but achievement remains active */ + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + } + + /* toggle hardcore mode should retain active achievements */ + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 2); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[1].trigger->state, RC_TRIGGER_STATE_ACTIVE); + } + + /* toggle hardcore mode should retain active achievements */ + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 2); + + /* trigger an achievement */ + achievement_info = (rc_client_achievement_info_t*)rc_client_get_achievement_info(g_client, 5501); + achievement_info->public.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED; + g_client->game->runtime.triggers[0].trigger->state = RC_TRIGGER_STATE_TRIGGERED; + + /* toggle hardcore mode should retain active achievements */ + rc_client_set_hardcore_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 0); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); /* only one active now */ + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + ASSERT_NUM_EQUALS(g_client->game->runtime.triggers[0].trigger->state, RC_TRIGGER_STATE_ACTIVE); + ASSERT_PTR_EQUALS(((rc_client_achievement_info_t*)achievement)->trigger, g_client->game->runtime.triggers[0].trigger); + } + + /* toggle hardcore mode should retain active achievements */ + rc_client_set_hardcore_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + ASSERT_NUM_EQUALS(rc_client_get_hardcore_enabled(g_client), 1); + ASSERT_NUM_EQUALS(g_client->game->runtime.trigger_count, 1); + + rc_client_destroy(g_client); +} + +static void test_set_encore_mode_enable(void) +{ + const rc_client_achievement_t* achievement; + + g_client = mock_client_logged_in(); + rc_client_set_encore_mode_enabled(g_client, 1); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501, unlock_5501_and_5502); + + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); /* unlock information still tracked */ + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); /* but achievement remains active */ + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + + /* toggle encore mode with a game loaded has no effect */ + rc_client_set_encore_mode_enabled(g_client, 0); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 0); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + + rc_client_destroy(g_client); +} + +static void test_set_encore_mode_disable(void) +{ + const rc_client_achievement_t* achievement; + + g_client = mock_client_logged_in(); + rc_client_set_encore_mode_enabled(g_client, 1); + rc_client_set_encore_mode_enabled(g_client, 0); + mock_client_load_game(patchdata_2ach_1lbd, unlock_5501, unlock_5501_and_5502); + + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 0); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + + /* toggle encore mode with a game loaded has no effect */ + rc_client_set_encore_mode_enabled(g_client, 1); + ASSERT_NUM_EQUALS(rc_client_get_encore_mode_enabled(g_client), 1); + + achievement = rc_client_get_achievement_info(g_client, 5501); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + } + achievement = rc_client_get_achievement_info(g_client, 5502); + ASSERT_PTR_NOT_NULL(achievement); + if (achievement) { + ASSERT_NUM_EQUALS(achievement->unlocked, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); + ASSERT_NUM_EQUALS(achievement->state, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE); + } + + rc_client_destroy(g_client); +} + +/* ----- harness ----- */ + +void test_client(void) { + TEST_SUITE_BEGIN(); + + /* login */ + TEST(test_login_with_password); + TEST(test_login_with_token); + TEST(test_login_required_fields); + TEST(test_login_with_incorrect_password); + TEST(test_login_incomplete_response); + TEST(test_login_with_password_async); + TEST(test_login_with_password_async_aborted); + + /* logout */ + TEST(test_logout); + TEST(test_logout_with_game_loaded); + TEST(test_logout_during_login); + TEST(test_logout_during_fetch_game); + + /* user */ + TEST(test_user_get_image_url); + + TEST(test_get_user_game_summary); + TEST(test_get_user_game_summary_softcore); + TEST(test_get_user_game_summary_encore_mode); + TEST(test_get_user_game_summary_with_unsupported_and_unofficial); + + /* load game */ + TEST(test_load_game_required_fields); + TEST(test_load_game_unknown_hash); + TEST(test_load_game_not_logged_in); + TEST(test_load_game); + TEST(test_load_game_async_login); + TEST(test_load_game_async_login_with_incorrect_password); + TEST(test_load_game_gameid_failure); + TEST(test_load_game_patch_failure); + TEST(test_load_game_postactivity_failure); + TEST(test_load_game_softcore_unlocks_failure); + TEST(test_load_game_hardcore_unlocks_failure); + TEST(test_load_game_gameid_aborted); + TEST(test_load_game_patch_aborted); + TEST(test_load_game_postactivity_aborted); + TEST(test_load_game_softcore_unlocks_aborted); + TEST(test_load_game_hardcore_unlocks_aborted); + TEST(test_load_game_while_spectating); + + /* identify and load game */ + TEST(test_identify_and_load_game_required_fields); + TEST(test_identify_and_load_game_console_specified); + TEST(test_identify_and_load_game_console_not_specified); + TEST(test_identify_and_load_game_unknown_hash); + TEST(test_identify_and_load_game_multihash); + TEST(test_identify_and_load_game_multihash_unknown_game); + TEST(test_identify_and_load_game_multihash_differ); + + /* change media */ + TEST(test_change_media_required_fields); + TEST(test_change_media_no_game_loaded); + TEST(test_change_media_same_game); + TEST(test_change_media_known_game); + TEST(test_change_media_unknown_game); + TEST(test_change_media_unhashable); + TEST(test_change_media_back_and_forth); + TEST(test_change_media_while_loading); + TEST(test_change_media_while_loading_later); + TEST(test_change_media_aborted); + + /* game */ + TEST(test_game_get_image_url); + TEST(test_game_get_image_url_non_ssl); + TEST(test_game_get_image_url_custom); + + /* subset */ + TEST(test_load_subset); + + /* achievements */ + TEST(test_achievement_list_simple); + TEST(test_achievement_list_simple_with_unlocks); + TEST(test_achievement_list_simple_with_unlocks_encore_mode); + TEST(test_achievement_list_simple_with_unofficial_and_unsupported); + TEST(test_achievement_list_simple_with_unofficial_off); + TEST(test_achievement_list_buckets); + TEST(test_achievement_list_subset_with_unofficial_and_unsupported); + TEST(test_achievement_list_subset_buckets); + TEST(test_achievement_list_subset_buckets_subset_first); + + TEST(test_achievement_get_image_url); + + /* leaderboards */ + TEST(test_leaderboard_list_simple); + TEST(test_leaderboard_list_simple_with_unsupported); + TEST(test_leaderboard_list_buckets); + TEST(test_leaderboard_list_buckets_with_unsupported); + TEST(test_leaderboard_list_subset); + + TEST(test_fetch_leaderboard_entries); + TEST(test_fetch_leaderboard_entries_no_user); + TEST(test_fetch_leaderboard_entries_around_user); + TEST(test_fetch_leaderboard_entries_around_user_not_logged_in); + TEST(test_fetch_leaderboard_entries_aborted); + + /* do frame */ + TEST(test_do_frame_bounds_check_system); + TEST(test_do_frame_bounds_check_available); + TEST(test_do_frame_achievement_trigger); + TEST(test_do_frame_achievement_trigger_already_awarded); + TEST(test_do_frame_achievement_trigger_server_error); + TEST(test_do_frame_achievement_trigger_while_spectating); + TEST(test_do_frame_achievement_trigger_automatic_retry); + TEST(test_do_frame_achievement_trigger_subset); + TEST(test_do_frame_achievement_measured); + TEST(test_do_frame_achievement_measured_progress_event); + TEST(test_do_frame_achievement_challenge_indicator); + TEST(test_do_frame_mastery); + TEST(test_do_frame_mastery_encore); + TEST(test_do_frame_leaderboard_started); + TEST(test_do_frame_leaderboard_update); + TEST(test_do_frame_leaderboard_failed); + TEST(test_do_frame_leaderboard_submit); + TEST(test_do_frame_leaderboard_submit_server_error); + TEST(test_do_frame_leaderboard_submit_while_spectating); + TEST(test_do_frame_leaderboard_tracker_sharing); + TEST(test_do_frame_leaderboard_tracker_sharing_hits); + TEST(test_do_frame_leaderboard_submit_automatic_retry); + + TEST(test_idle_ping); + TEST(test_do_frame_ping_rich_presence); + + TEST(test_reset_hides_widgets); + TEST(test_deserialize_progress_updates_widgets); + TEST(test_deserialize_progress_null); + TEST(test_deserialize_progress_invalid); + + /* processing required */ + TEST(test_processing_required); + TEST(test_processing_required_empty_game); + TEST(test_processing_required_rich_presence_only); + TEST(test_processing_required_leaderboard_only); + TEST(test_processing_required_after_mastery); + TEST(test_processing_required_after_mastery_no_leaderboards); + + /* settings */ + TEST(test_set_hardcore_disable); + TEST(test_set_hardcore_disable_active_tracker); + TEST(test_set_hardcore_enable); + TEST(test_set_hardcore_enable_no_game_loaded); + TEST(test_set_hardcore_enable_encore_mode); + TEST(test_set_encore_mode_enable); + TEST(test_set_encore_mode_disable); + + TEST_SUITE_END(); +} diff --git a/test/rcheevos/test_rc_libretro.c b/test/rcheevos/test_rc_libretro.c index c8c0341b..93dbb646 100644 --- a/test/rcheevos/test_rc_libretro.c +++ b/test/rcheevos/test_rc_libretro.c @@ -49,7 +49,16 @@ static void test_disallowed_system(const char* library_name, int console_id) { static void test_memory_init_without_regions() { rc_libretro_memory_regions_t regions; unsigned avail; - unsigned char buffer1[16], buffer2[8]; + unsigned char buffer1[16], buffer2[8], buffer3[4]; + int i; + + for (i = 0; i < sizeof(buffer1); ++i) + buffer1[i] = i; + for (i = 0; i < sizeof(buffer2); ++i) + buffer2[i] = i; + for (i = 0; i < sizeof(buffer3); ++i) + buffer3[i] = i; + retro_memory_data[RETRO_MEMORY_SYSTEM_RAM] = buffer1; retro_memory_size[RETRO_MEMORY_SYSTEM_RAM] = sizeof(buffer1); retro_memory_data[RETRO_MEMORY_SAVE_RAM] = buffer2; @@ -71,6 +80,13 @@ static void test_memory_init_without_regions() { ASSERT_NUM_EQUALS(avail, sizeof(buffer2) - 2); ASSERT_PTR_NULL(rc_libretro_memory_find_avail(®ions, sizeof(buffer1) + sizeof(buffer2) + 2, &avail)); ASSERT_NUM_EQUALS(avail, 0); + + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 2, buffer3, 1), 1); + ASSERT_TRUE(memcmp(buffer3, &buffer1[2], 1) == 0); + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, 7, buffer3, 4), 4); + ASSERT_TRUE(memcmp(buffer3, &buffer1[7], 4) == 0); + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, sizeof(buffer1) - 2, buffer3, 3), 2); + ASSERT_NUM_EQUALS(rc_libretro_memory_read(®ions, sizeof(buffer1) + sizeof(buffer2) + 2, buffer3, 1), 0); } static void test_memory_init_without_regions_system_ram_only() { diff --git a/test/rhash/data.c b/test/rhash/data.c index 816dd3f8..80913def 100644 --- a/test/rhash/data.c +++ b/test/rhash/data.c @@ -692,7 +692,7 @@ uint8_t* generate_jaguarcd_bin(unsigned header_offset, unsigned binary_size, int { size_t size_needed = (((binary_size + 64 + 32 + 8) + 2351) / 2352) * 2352; uint8_t* image = (uint8_t*)calloc(size_needed, 1); - int i; + size_t i; if (!image) return NULL; diff --git a/test/test.c b/test/test.c index dad5edc2..6a8f1b4c 100644 --- a/test/test.c +++ b/test/test.c @@ -59,6 +59,7 @@ extern void test_lboard(); extern void test_richpresence(); extern void test_runtime(); extern void test_runtime_progress(); +extern void test_client(); extern void test_consoleinfo(); extern void test_rc_libretro(); @@ -94,6 +95,7 @@ int main(void) { test_richpresence(); test_runtime(); test_runtime_progress(); + test_client(); test_consoleinfo(); test_rc_libretro();