From 5d750a8803444c488d432494506e379f5064acf6 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 9 Aug 2023 19:39:42 +1000 Subject: [PATCH] dep: Bump rcheevos to 3af1e2fc5188d6e932ee379942f4049ea877e648 --- dep/rcheevos/include/rc_api_editor.h | 6 + dep/rcheevos/include/rc_api_info.h | 3 + dep/rcheevos/include/rc_api_request.h | 33 +- dep/rcheevos/include/rc_api_runtime.h | 7 + dep/rcheevos/include/rc_api_user.h | 5 + dep/rcheevos/include/rc_client.h | 584 +++ dep/rcheevos/include/rc_consoles.h | 6 + dep/rcheevos/include/rc_error.h | 8 +- dep/rcheevos/include/rc_hash.h | 17 +- dep/rcheevos/include/rc_runtime.h | 6 +- dep/rcheevos/include/rc_runtime_types.h | 7 +- dep/rcheevos/src/rapi/rc_api_common.c | 504 +- dep/rcheevos/src/rapi/rc_api_common.h | 26 +- dep/rcheevos/src/rapi/rc_api_editor.c | 157 +- dep/rcheevos/src/rapi/rc_api_info.c | 149 +- dep/rcheevos/src/rapi/rc_api_runtime.c | 225 +- dep/rcheevos/src/rapi/rc_api_user.c | 76 +- dep/rcheevos/src/rcheevos/alloc.c | 18 +- dep/rcheevos/src/rcheevos/compat.c | 94 +- dep/rcheevos/src/rcheevos/condition.c | 295 +- dep/rcheevos/src/rcheevos/condset.c | 4 +- dep/rcheevos/src/rcheevos/consoleinfo.c | 126 +- dep/rcheevos/src/rcheevos/memref.c | 26 +- dep/rcheevos/src/rcheevos/operand.c | 10 + dep/rcheevos/src/rcheevos/rc_client.c | 4669 +++++++++++++++++ .../src/rcheevos/rc_client_internal.h | 290 + dep/rcheevos/src/rcheevos/rc_compat.h | 39 + dep/rcheevos/src/rcheevos/rc_internal.h | 27 + dep/rcheevos/src/rcheevos/rc_libretro.c | 816 +++ dep/rcheevos/src/rcheevos/rc_libretro.h | 92 + dep/rcheevos/src/rcheevos/rc_validate.c | 847 +++ dep/rcheevos/src/rcheevos/rc_validate.h | 26 + dep/rcheevos/src/rcheevos/rc_version.h | 29 + dep/rcheevos/src/rcheevos/richpresence.c | 56 +- dep/rcheevos/src/rcheevos/runtime.c | 67 +- dep/rcheevos/src/rcheevos/runtime_progress.c | 31 +- dep/rcheevos/src/rcheevos/trigger.c | 22 +- dep/rcheevos/src/rcheevos/value.c | 35 + dep/rcheevos/src/rhash/cdreader.c | 49 +- dep/rcheevos/src/rhash/hash.c | 514 +- dep/rcheevos/src/rhash/md5.c | 3 +- 41 files changed, 9440 insertions(+), 564 deletions(-) create mode 100644 dep/rcheevos/include/rc_client.h create mode 100644 dep/rcheevos/src/rcheevos/rc_client.c create mode 100644 dep/rcheevos/src/rcheevos/rc_client_internal.h create mode 100644 dep/rcheevos/src/rcheevos/rc_libretro.c create mode 100644 dep/rcheevos/src/rcheevos/rc_libretro.h create mode 100644 dep/rcheevos/src/rcheevos/rc_validate.c create mode 100644 dep/rcheevos/src/rcheevos/rc_validate.h create mode 100644 dep/rcheevos/src/rcheevos/rc_version.h diff --git a/dep/rcheevos/include/rc_api_editor.h b/dep/rcheevos/include/rc_api_editor.h index 2bda50325..46880d582 100644 --- a/dep/rcheevos/include/rc_api_editor.h +++ b/dep/rcheevos/include/rc_api_editor.h @@ -44,6 +44,7 @@ rc_api_fetch_code_notes_response_t; int rc_api_init_fetch_code_notes_request(rc_api_request_t* request, const rc_api_fetch_code_notes_request_t* api_params); int rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response, const char* server_response); +int rc_api_process_fetch_code_notes_server_response(rc_api_fetch_code_notes_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response); /* --- Update Code Note --- */ @@ -76,6 +77,7 @@ rc_api_update_code_note_response_t; int rc_api_init_update_code_note_request(rc_api_request_t* request, const rc_api_update_code_note_request_t* api_params); int rc_api_process_update_code_note_response(rc_api_update_code_note_response_t* response, const char* server_response); +int rc_api_process_update_code_note_server_response(rc_api_update_code_note_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_update_code_note_response(rc_api_update_code_note_response_t* response); /* --- Update Achievement --- */ @@ -121,6 +123,7 @@ rc_api_update_achievement_response_t; int rc_api_init_update_achievement_request(rc_api_request_t* request, const rc_api_update_achievement_request_t* api_params); int rc_api_process_update_achievement_response(rc_api_update_achievement_response_t* response, const char* server_response); +int rc_api_process_update_achievement_server_response(rc_api_update_achievement_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_update_achievement_response(rc_api_update_achievement_response_t* response); /* --- Update Leaderboard --- */ @@ -170,6 +173,7 @@ rc_api_update_leaderboard_response_t; int rc_api_init_update_leaderboard_request(rc_api_request_t* request, const rc_api_update_leaderboard_request_t* api_params); int rc_api_process_update_leaderboard_response(rc_api_update_leaderboard_response_t* response, const char* server_response); +int rc_api_process_update_leaderboard_server_response(rc_api_update_leaderboard_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_update_leaderboard_response(rc_api_update_leaderboard_response_t* response); /* --- Fetch Badge Range --- */ @@ -199,6 +203,7 @@ rc_api_fetch_badge_range_response_t; int rc_api_init_fetch_badge_range_request(rc_api_request_t* request, const rc_api_fetch_badge_range_request_t* api_params); int rc_api_process_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response, const char* server_response); +int rc_api_process_fetch_badge_range_server_response(rc_api_fetch_badge_range_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response); /* --- Add Game Hash --- */ @@ -238,6 +243,7 @@ rc_api_add_game_hash_response_t; int rc_api_init_add_game_hash_request(rc_api_request_t* request, const rc_api_add_game_hash_request_t* api_params); int rc_api_process_add_game_hash_response(rc_api_add_game_hash_response_t* response, const char* server_response); +int rc_api_process_add_game_hash_server_response(rc_api_add_game_hash_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_add_game_hash_response(rc_api_add_game_hash_response_t* response); #ifdef __cplusplus diff --git a/dep/rcheevos/include/rc_api_info.h b/dep/rcheevos/include/rc_api_info.h index 7979cc391..7d3a31607 100644 --- a/dep/rcheevos/include/rc_api_info.h +++ b/dep/rcheevos/include/rc_api_info.h @@ -64,6 +64,7 @@ rc_api_fetch_achievement_info_response_t; int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params); int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response); +int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achievement_info_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response); /* --- Fetch Leaderboard Info --- */ @@ -135,6 +136,7 @@ rc_api_fetch_leaderboard_info_response_t; int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params); int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response); +int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboard_info_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response); /* --- Fetch Games List --- */ @@ -173,6 +175,7 @@ rc_api_fetch_games_list_response_t; int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params); int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response); +int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response); #ifdef __cplusplus diff --git a/dep/rcheevos/include/rc_api_request.h b/dep/rcheevos/include/rc_api_request.h index 8ba482a8f..81c855fd3 100644 --- a/dep/rcheevos/include/rc_api_request.h +++ b/dep/rcheevos/include/rc_api_request.h @@ -3,6 +3,8 @@ #include "rc_error.h" +#include + #ifdef __cplusplus extern "C" { #endif @@ -10,15 +12,25 @@ extern "C" { /** * A block of memory for variable length data (like strings and arrays). */ -typedef struct rc_api_buffer_t { +typedef struct rc_api_buffer_chunk_t { /* The current location where data is being written */ char* write; /* The first byte past the end of data where writing cannot occur */ char* end; + /* The first byte of the data */ + char* start; /* The next block in the allocated memory chain */ - struct rc_api_buffer_t* next; - /* The buffer containing the data. The actual size may be larger than 256 bytes for buffers allocated in - * the next chain. The 256 byte size specified is for the initial allocation within the container object. */ + struct rc_api_buffer_chunk_t* next; +} +rc_api_buffer_chunk_t; + +/** + * A preallocated block of memory for variable length data (like strings and arrays). + */ +typedef struct rc_api_buffer_t { + /* The chunk data (will point at the local data member) */ + struct rc_api_buffer_chunk_t chunk; + /* Small chunk of memory pre-allocated for the chunk */ char data[256]; } rc_api_buffer_t; @@ -31,6 +43,8 @@ typedef struct rc_api_request_t { const char* url; /* Additional query args that should be sent via a POST command. If null, GET may be used */ const char* post_data; + /* The HTTP Content-Type of the POST data. */ + const char* content_type; /* Storage for the url and post_data */ rc_api_buffer_t buffer; @@ -41,7 +55,7 @@ rc_api_request_t; * Common attributes for all server responses. */ typedef struct rc_api_response_t { - /* Server-provided success indicator (non-zero on failure) */ + /* Server-provided success indicator (non-zero on success, zero on failure) */ int succeeded; /* Server-provided message associated to the failure */ const char* error_message; @@ -56,6 +70,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/dep/rcheevos/include/rc_api_runtime.h b/dep/rcheevos/include/rc_api_runtime.h index 68f56fd51..4534ff182 100644 --- a/dep/rcheevos/include/rc_api_runtime.h +++ b/dep/rcheevos/include/rc_api_runtime.h @@ -59,6 +59,7 @@ rc_api_resolve_hash_response_t; int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params); int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response); +int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response); /* --- Fetch Game Data --- */ @@ -155,6 +156,7 @@ rc_api_fetch_game_data_response_t; int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params); int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response); +int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response); /* --- Ping --- */ @@ -185,6 +187,7 @@ rc_api_ping_response_t; int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params); int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response); +int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_ping_response(rc_api_ping_response_t* response); /* --- Award Achievement --- */ @@ -214,6 +217,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; @@ -225,6 +230,7 @@ rc_api_award_achievement_response_t; int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params); int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response); +int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response); /* --- Submit Leaderboard Entry --- */ @@ -282,6 +288,7 @@ rc_api_submit_lboard_entry_response_t; int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params); int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response); +int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response); #ifdef __cplusplus diff --git a/dep/rcheevos/include/rc_api_user.h b/dep/rcheevos/include/rc_api_user.h index 758842557..8f96044c8 100644 --- a/dep/rcheevos/include/rc_api_user.h +++ b/dep/rcheevos/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 */ @@ -45,6 +47,7 @@ rc_api_login_response_t; int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params); int rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response); +int rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_login_response(rc_api_login_response_t* response); /* --- Start Session --- */ @@ -73,6 +76,7 @@ rc_api_start_session_response_t; int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_request_t* api_params); int rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response); +int rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response); /* --- Fetch User Unlocks --- */ @@ -108,6 +112,7 @@ rc_api_fetch_user_unlocks_response_t; int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params); int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response); +int rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response); void rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response); #ifdef __cplusplus diff --git a/dep/rcheevos/include/rc_client.h b/dep/rcheevos/include/rc_client.h new file mode 100644 index 000000000..40e9a977a --- /dev/null +++ b/dep/rcheevos/include/rc_client.h @@ -0,0 +1,584 @@ +#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* client); + +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] */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE = 8, /* progress indicator should be hidden */ + RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE = 9, /* progress indicator should be updated to reflect new badge/progress for [achievement] */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW = 10, /* [leaderboard_tracker] should be shown */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE = 11, /* [leaderboard_tracker] should be hidden */ + RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE = 12, /* [leaderboard_tracker] updated */ + RC_CLIENT_EVENT_RESET = 13, /* emulated system should be reset (as the result of enabling hardcore) */ + RC_CLIENT_EVENT_GAME_COMPLETED = 14, /* all achievements for the game have been earned */ + RC_CLIENT_EVENT_SERVER_ERROR = 15 /* 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/dep/rcheevos/include/rc_consoles.h b/dep/rcheevos/include/rc_consoles.h index 21c369c0a..0dd9c1ee1 100644 --- a/dep/rcheevos/include/rc_consoles.h +++ b/dep/rcheevos/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, @@ -85,6 +86,11 @@ enum { RC_CONSOLE_ARCADIA_2001 = 73, RC_CONSOLE_INTERTON_VC_4000 = 74, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER = 75, + RC_CONSOLE_PC_ENGINE_CD = 76, + RC_CONSOLE_ATARI_JAGUAR_CD = 77, + RC_CONSOLE_NINTENDO_DSI = 78, + RC_CONSOLE_TI83 = 79, + RC_CONSOLE_UZEBOX = 80, RC_CONSOLE_HUBS = 100, RC_CONSOLE_EVENTS = 101 diff --git a/dep/rcheevos/include/rc_error.h b/dep/rcheevos/include/rc_error.h index 91a98c11c..b80bc0bab 100644 --- a/dep/rcheevos/include/rc_error.h +++ b/dep/rcheevos/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/dep/rcheevos/include/rc_hash.h b/dep/rcheevos/include/rc_hash.h index b5fd59bab..ba9ea1c02 100644 --- a/dep/rcheevos/include/rc_hash.h +++ b/dep/rcheevos/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 */ @@ -92,11 +92,12 @@ extern "C" { /* ===================================================== */ - #define RC_HASH_CDTRACK_FIRST_DATA ((uint32_t)-1) - #define RC_HASH_CDTRACK_LAST ((uint32_t)-2) - #define RC_HASH_CDTRACK_LARGEST ((uint32_t)-3) + #define RC_HASH_CDTRACK_FIRST_DATA ((uint32_t)-1) /* the first data track (skip audio tracks) */ + #define RC_HASH_CDTRACK_LAST ((uint32_t)-2) /* the last data/audio track */ + #define RC_HASH_CDTRACK_LARGEST ((uint32_t)-3) /* the largest data/audio track */ + #define RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION ((uint32_t)-4) /* the first data/audio track of the second session */ - /* opens a track from the specified file. track 0 indicates the largest data track should be opened. + /* opens a track from the specified file. see the RC_HASH_CDTRACK_ defines for special tracks. * returns a handle to be passed to the other functions, or NULL if the track could not be opened. */ typedef void* (*rc_hash_cdreader_open_track_handler)(const char* path, uint32_t track); diff --git a/dep/rcheevos/include/rc_runtime.h b/dep/rcheevos/include/rc_runtime.h index 6ad5f0266..d7a25c707 100644 --- a/dep/rcheevos/include/rc_runtime.h +++ b/dep/rcheevos/include/rc_runtime.h @@ -88,9 +88,12 @@ typedef struct rc_runtime_t { rc_value_t* variables; rc_value_t** next_variable; + + char owns_self; } rc_runtime_t; +rc_runtime_t* rc_runtime_alloc(void); void rc_runtime_init(rc_runtime_t* runtime); void rc_runtime_destroy(rc_runtime_t* runtime); @@ -121,7 +124,8 @@ enum { RC_RUNTIME_EVENT_LBOARD_TRIGGERED, RC_RUNTIME_EVENT_ACHIEVEMENT_DISABLED, RC_RUNTIME_EVENT_LBOARD_DISABLED, - RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED + RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED, + RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED }; typedef struct rc_runtime_event_t { diff --git a/dep/rcheevos/include/rc_runtime_types.h b/dep/rcheevos/include/rc_runtime_types.h index c446ab730..dc3ab60c9 100644 --- a/dep/rcheevos/include/rc_runtime_types.h +++ b/dep/rcheevos/include/rc_runtime_types.h @@ -56,6 +56,7 @@ enum { RC_MEMSIZE_32_BITS_BE, RC_MEMSIZE_FLOAT, RC_MEMSIZE_MBF32, + RC_MEMSIZE_MBF32_LE, RC_MEMSIZE_VARIABLE }; @@ -169,7 +170,8 @@ enum { RC_OPERATOR_NONE, RC_OPERATOR_MULT, RC_OPERATOR_DIV, - RC_OPERATOR_AND + RC_OPERATOR_AND, + RC_OPERATOR_XOR }; typedef struct rc_condition_t rc_condition_t; @@ -198,6 +200,9 @@ struct rc_condition_t { /* Whether or not the condition evaluated true on the last check */ char is_true; + + /* Unique identifier of optimized comparator to use */ + char optimized_comparator; }; /*****************************************************************************\ diff --git a/dep/rcheevos/src/rapi/rc_api_common.c b/dep/rcheevos/src/rapi/rc_api_common.c index c703af752..7fdc7beb1 100644 --- a/dep/rcheevos/src/rapi/rc_api_common.c +++ b/dep/rcheevos/src/rapi/rc_api_common.c @@ -10,7 +10,9 @@ #include #define RETROACHIEVEMENTS_HOST "https://retroachievements.org" -#define RETROACHIEVEMENTS_IMAGE_HOST "http://i.retroachievements.org" +#define RETROACHIEVEMENTS_IMAGE_HOST "https://media.retroachievements.org" +#define RETROACHIEVEMENTS_HOST_NONSSL "http://retroachievements.org" +#define RETROACHIEVEMENTS_IMAGE_HOST_NONSSL "http://media.retroachievements.org" static char* g_host = NULL; static char* g_imagehost = NULL; @@ -18,157 +20,174 @@ static char* g_imagehost = NULL; /* --- rc_json --- */ -static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen); -static int rc_json_parse_array(const char** json_ptr, rc_json_field_t* field); +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen); +static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field); -static int rc_json_parse_field(const char** json_ptr, rc_json_field_t* field) { +static int rc_json_match_char(rc_json_iterator_t* iterator, char c) +{ + if (iterator->json < iterator->end && *iterator->json == c) { + ++iterator->json; + return 1; + } + + return 0; +} + +static void rc_json_skip_whitespace(rc_json_iterator_t* iterator) +{ + while (iterator->json < iterator->end && isspace((unsigned char)*iterator->json)) + ++iterator->json; +} + +static int rc_json_find_closing_quote(rc_json_iterator_t* iterator) +{ + while (iterator->json < iterator->end) { + if (*iterator->json == '"') + return 1; + + if (*iterator->json == '\\') { + ++iterator->json; + if (iterator->json == iterator->end) + return 0; + } + + if (*iterator->json == '\0') + return 0; + + ++iterator->json; + } + + return 0; +} + +static int rc_json_parse_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { int result; - field->value_start = *json_ptr; + if (iterator->json >= iterator->end) + return RC_INVALID_JSON; - switch (**json_ptr) + field->value_start = iterator->json; + + switch (*iterator->json) { case '"': /* quoted string */ - ++(*json_ptr); - while (**json_ptr != '"') { - if (**json_ptr == '\\') - ++(*json_ptr); - - if (**json_ptr == '\0') - return RC_INVALID_JSON; - - ++(*json_ptr); - } - ++(*json_ptr); + ++iterator->json; + if (!rc_json_find_closing_quote(iterator)) + return RC_INVALID_JSON; + ++iterator->json; break; case '-': case '+': /* signed number */ - ++(*json_ptr); + ++iterator->json; /* fallthrough to number */ case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': /* number */ - do { - ++(*json_ptr); - } while (**json_ptr >= '0' && **json_ptr <= '9'); - if (**json_ptr == '.') { - do { - ++(*json_ptr); - } while (**json_ptr >= '0' && **json_ptr <= '9'); + while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9') + ++iterator->json; + + if (rc_json_match_char(iterator, '.')) { + while (iterator->json < iterator->end && *iterator->json >= '0' && *iterator->json <= '9') + ++iterator->json; } break; case '[': /* array */ - result = rc_json_parse_array(json_ptr, field); + result = rc_json_parse_array(iterator, field); if (result != RC_OK) - return result; + return result; break; case '{': /* object */ - result = rc_json_parse_object(json_ptr, NULL, 0, &field->array_size); + result = rc_json_parse_object(iterator, NULL, 0, &field->array_size); if (result != RC_OK) return result; break; default: /* non-quoted text [true,false,null] */ - if (!isalpha((unsigned char)**json_ptr)) + if (!isalpha((unsigned char)*iterator->json)) return RC_INVALID_JSON; - do { - ++(*json_ptr); - } while (isalnum((unsigned char)**json_ptr)); + while (iterator->json < iterator->end && isalnum((unsigned char)*iterator->json)) + ++iterator->json; break; } - field->value_end = *json_ptr; + field->value_end = iterator->json; return RC_OK; } -static int rc_json_parse_array(const char** json_ptr, rc_json_field_t* field) { +static int rc_json_parse_array(rc_json_iterator_t* iterator, rc_json_field_t* field) { rc_json_field_t unused_field; - const char* json = *json_ptr; int result; - if (*json != '[') + if (!rc_json_match_char(iterator, '[')) return RC_INVALID_JSON; - ++json; field->array_size = 0; - if (*json != ']') { - do - { - while (isspace((unsigned char)*json)) - ++json; - result = rc_json_parse_field(&json, &unused_field); - if (result != RC_OK) - return result; + if (rc_json_match_char(iterator, ']')) /* empty array */ + return RC_OK; - ++field->array_size; + do + { + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*json)) - ++json; + result = rc_json_parse_field(iterator, &unused_field); + if (result != RC_OK) + return result; - if (*json != ',') - break; + ++field->array_size; - ++json; - } while (1); + rc_json_skip_whitespace(iterator); + } while (rc_json_match_char(iterator, ',')); - if (*json != ']') - return RC_INVALID_JSON; - } + if (!rc_json_match_char(iterator, ']')) + return RC_INVALID_JSON; - *json_ptr = ++json; return RC_OK; } -static int rc_json_get_next_field(rc_json_object_field_iterator_t* iterator) { - const char* json = iterator->json; +static int rc_json_get_next_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*json)) - ++json; - - if (*json != '"') + if (!rc_json_match_char(iterator, '"')) return RC_INVALID_JSON; - iterator->field.name = ++json; - while (*json != '"') { - if (!*json) + field->name = iterator->json; + while (iterator->json < iterator->end && *iterator->json != '"') { + if (!*iterator->json) return RC_INVALID_JSON; - ++json; + ++iterator->json; } - iterator->name_len = json - iterator->field.name; - ++json; - while (isspace((unsigned char)*json)) - ++json; - - if (*json != ':') + if (iterator->json == iterator->end) return RC_INVALID_JSON; - ++json; + field->name_len = iterator->json - field->name; + ++iterator->json; - while (isspace((unsigned char)*json)) - ++json; + rc_json_skip_whitespace(iterator); - if (rc_json_parse_field(&json, &iterator->field) < 0) + if (!rc_json_match_char(iterator, ':')) return RC_INVALID_JSON; - while (isspace((unsigned char)*json)) - ++json; + rc_json_skip_whitespace(iterator); + + if (rc_json_parse_field(iterator, field) < 0) + return RC_INVALID_JSON; + + rc_json_skip_whitespace(iterator); - iterator->json = json; return RC_OK; } -static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen) { - rc_json_object_field_iterator_t iterator; - const char* json = *json_ptr; +static int rc_json_parse_object(rc_json_iterator_t* iterator, rc_json_field_t* fields, size_t field_count, unsigned* fields_seen) { size_t i; unsigned num_fields = 0; + rc_json_field_t field; int result; if (fields_seen) @@ -177,60 +196,105 @@ static int rc_json_parse_object(const char** json_ptr, rc_json_field_t* fields, for (i = 0; i < field_count; ++i) fields[i].value_start = fields[i].value_end = NULL; - if (*json != '{') + if (!rc_json_match_char(iterator, '{')) return RC_INVALID_JSON; - ++json; - if (*json == '}') { - *json_ptr = ++json; + if (rc_json_match_char(iterator, '}')) /* empty object */ return RC_OK; - } - - memset(&iterator, 0, sizeof(iterator)); - iterator.json = json; do { - result = rc_json_get_next_field(&iterator); + result = rc_json_get_next_field(iterator, &field); if (result != RC_OK) return result; for (i = 0; i < field_count; ++i) { - if (!fields[i].value_start && strncmp(fields[i].name, iterator.field.name, iterator.name_len) == 0 && - fields[i].name[iterator.name_len] == '\0') { - fields[i].value_start = iterator.field.value_start; - fields[i].value_end = iterator.field.value_end; - fields[i].array_size = iterator.field.array_size; + if (!fields[i].value_start && fields[i].name_len == field.name_len && + memcmp(fields[i].name, field.name, field.name_len) == 0) { + fields[i].value_start = field.value_start; + fields[i].value_end = field.value_end; + fields[i].array_size = field.array_size; break; } } ++num_fields; - if (*iterator.json != ',') - break; - ++iterator.json; - } while (1); + } while (rc_json_match_char(iterator, ',')); - if (*iterator.json != '}') + if (!rc_json_match_char(iterator, '}')) return RC_INVALID_JSON; if (fields_seen) *fields_seen = num_fields; - *json_ptr = ++iterator.json; return RC_OK; } -int rc_json_get_next_object_field(rc_json_object_field_iterator_t* iterator) { - if (*iterator->json != ',' && *iterator->json != '{') +int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field) { + if (!rc_json_match_char(iterator, ',') && !rc_json_match_char(iterator, '{')) return 0; - ++iterator->json; - return (rc_json_get_next_field(iterator) == RC_OK); + return (rc_json_get_next_field(iterator, field) == RC_OK); } -int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_json_field_t* fields, size_t field_count) { +int rc_json_get_object_string_length(const char* json) { + const char* json_start = json; + + rc_json_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = json; + iterator.end = json + (1024 * 1024 * 1024); /* arbitrary 1GB limit on JSON response */ + + rc_json_parse_object(&iterator, NULL, 0, NULL); + + return (int)(iterator.json - json_start); +} + +static int rc_json_extract_html_error(rc_api_response_t* response, const rc_api_server_response_t* server_response) { + const char* json = server_response->body; + const char* end = json; + + const char* title_start = strstr(json, ""); + if (title_start) { + title_start += 7; + if (isdigit((int)*title_start)) { + const char* title_end = strstr(title_start + 7, ""); + if (title_end) { + char* dst = rc_buf_reserve(&response->buffer, (title_end - title_start) + 1); + response->error_message = dst; + memcpy(dst, title_start, title_end - title_start); + dst += (title_end - title_start); + *dst++ = '\0'; + rc_buf_consume(&response->buffer, response->error_message, dst); + response->succeeded = 0; + return RC_INVALID_JSON; + } + } + } + + while (*end && *end != '\n' && end - json < 200) + ++end; + + if (end > json && end[-1] == '\r') + --end; + + if (end > json) { + char* dst = rc_buf_reserve(&response->buffer, (end - json) + 1); + response->error_message = dst; + memcpy(dst, json, end - json); + dst += (end - json); + *dst++ = '\0'; + rc_buf_consume(&response->buffer, response->error_message, dst); + } + + response->succeeded = 0; + return RC_INVALID_JSON; +} + +int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count) { + int result; + #ifndef NDEBUG if (field_count < 2) return RC_INVALID_STATE; @@ -240,37 +304,28 @@ int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_jso return RC_INVALID_STATE; #endif - if (*json == '{') { - int result = rc_json_parse_object(&json, fields, field_count, NULL); + response->error_message = NULL; + + if (!server_response || !server_response->body || !*server_response->body) { + response->succeeded = 0; + return RC_NO_RESPONSE; + } + + if (*server_response->body != '{') { + result = rc_json_extract_html_error(response, server_response); + } + else { + rc_json_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = server_response->body; + iterator.end = server_response->body + server_response->body_length; + result = rc_json_parse_object(&iterator, fields, field_count, NULL); rc_json_get_optional_string(&response->error_message, response, &fields[1], "Error", NULL); rc_json_get_optional_bool(&response->succeeded, &fields[0], "Success", 1); - - return result; } - response->error_message = NULL; - - if (*json) { - const char* end = json; - while (*end && *end != '\n' && end - json < 200) - ++end; - - if (end > json && end[-1] == '\r') - --end; - - if (end > json) { - char* dst = rc_buf_reserve(&response->buffer, (end - json) + 1); - response->error_message = dst; - memcpy(dst, json, end - json); - dst += (end - json); - *dst++ = '\0'; - rc_buf_consume(&response->buffer, response->error_message, dst); - } - } - - response->succeeded = 0; - return RC_INVALID_JSON; + return result; } static int rc_json_missing_field(rc_api_response_t* response, const rc_json_field_t* field) { @@ -293,42 +348,48 @@ static int rc_json_missing_field(rc_api_response_t* response, const rc_json_fiel } int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name) { - const char* json = field->value_start; + rc_json_iterator_t iterator; + #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif - if (!json) + if (!field->value_start) return rc_json_missing_field(response, field); - return (rc_json_parse_object(&json, fields, field_count, &field->array_size) == RC_OK); + memset(&iterator, 0, sizeof(iterator)); + iterator.json = field->value_start; + iterator.end = field->value_end; + return (rc_json_parse_object(&iterator, fields, field_count, &field->array_size) == RC_OK); } -static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_field_t* iterator) { - if (!iterator->array_size) +static int rc_json_get_array_entry_value(rc_json_field_t* field, rc_json_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); + + if (iterator->json >= iterator->end) return 0; - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + if (rc_json_parse_field(iterator, field) != RC_OK) + return 0; - rc_json_parse_field(&iterator->value_start, field); + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); - ++iterator->value_start; /* skip , or ] */ - - --iterator->array_size; return 1; } int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { - rc_json_field_t iterator; + rc_json_iterator_t iterator; + rc_json_field_t array; rc_json_field_t value; unsigned* entry; - if (!rc_json_get_required_array(num_entries, &iterator, response, field, field_name)) + if (!rc_json_get_required_array(num_entries, &array, response, field, field_name)) return RC_MISSING_VALUE; if (*num_entries) { @@ -338,6 +399,10 @@ int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, r value.name = field_name; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array.value_start; + iterator.end = array.value_end; + entry = *entries; while (rc_json_get_array_entry_value(&value, &iterator)) { if (!rc_json_get_unum(entry, &value, field_name)) @@ -353,10 +418,12 @@ int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, r return RC_OK; } -int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { +int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif if (!field->value_start || *field->value_start != '[') { @@ -364,28 +431,27 @@ int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, return rc_json_missing_field(response, field); } - memcpy(iterator, field, sizeof(*iterator)); - ++iterator->value_start; /* skip [ */ + memcpy(array_field, field, sizeof(*array_field)); + ++array_field->value_start; /* skip [ */ *num_entries = field->array_size; return 1; } -int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_field_t* iterator) { - if (!iterator->array_size) +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator) { + rc_json_skip_whitespace(iterator); + + if (iterator->json >= iterator->end) return 0; - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + if (rc_json_parse_object(iterator, fields, field_count, NULL) != RC_OK) + return 0; - rc_json_parse_object(&iterator->value_start, fields, field_count, NULL); + rc_json_skip_whitespace(iterator); - while (isspace((unsigned char)*iterator->value_start)) - ++iterator->value_start; + if (!rc_json_match_char(iterator, ',')) + rc_json_match_char(iterator, ']'); - ++iterator->value_start; /* skip , or ] */ - - --iterator->array_size; return 1; } @@ -451,6 +517,8 @@ int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_ #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif if (!src) { @@ -562,6 +630,8 @@ int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_na #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif if (!src) { @@ -613,6 +683,8 @@ int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* fi #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif if (!src) { @@ -654,11 +726,13 @@ int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif if (*field->value_start == '\"') { memset(&tm, 0, sizeof(tm)); - if (sscanf(field->value_start + 1, "%d-%d-%d %d:%d:%d", + if (sscanf_s(field->value_start + 1, "%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) { tm.tm_mon--; /* 0-based */ tm.tm_year -= 1900; /* 1900 based */ @@ -669,9 +743,11 @@ int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* * timezone conversion twice and manually removing the difference */ { time_t local_timet = mktime(&tm); - struct tm* gmt_tm = gmtime(&local_timet); - time_t skewed_timet = mktime(gmt_tm); /* applies local time adjustment second time */ - time_t tz_offset = skewed_timet - local_timet; + time_t skewed_timet, tz_offset; + struct tm gmt_tm; + gmtime_s(&gmt_tm, &local_timet); + skewed_timet = mktime(&gmt_tm); /* applies local time adjustment second time */ + tz_offset = skewed_timet - local_timet; *out = local_timet - tz_offset; } @@ -696,6 +772,8 @@ int rc_json_get_bool(int* out, const rc_json_field_t* field, const char* field_n #ifndef NDEBUG if (strcmp(field->name, field_name) != 0) return 0; +#else + (void)field_name; #endif if (src) { @@ -731,31 +809,32 @@ int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_js /* --- rc_buf --- */ void rc_buf_init(rc_api_buffer_t* buffer) { - buffer->write = &buffer->data[0]; - buffer->end = &buffer->data[sizeof(buffer->data)]; - buffer->next = NULL; + buffer->chunk.write = buffer->chunk.start = &buffer->data[0]; + buffer->chunk.end = &buffer->data[sizeof(buffer->data)]; + buffer->chunk.next = NULL; } void rc_buf_destroy(rc_api_buffer_t* buffer) { + rc_api_buffer_chunk_t *chunk; #ifdef DEBUG_BUFFERS int count = 0; int wasted = 0; int total = 0; #endif - /* first buffer is not allocated */ - buffer = buffer->next; + /* first chunk is not allocated. skip it. */ + chunk = buffer->chunk.next; /* deallocate any additional buffers */ - while (buffer) { - rc_api_buffer_t* next = buffer->next; + while (chunk) { + rc_api_buffer_chunk_t* next = chunk->next; #ifdef DEBUG_BUFFERS - total += (int)(buffer->end - buffer->data); - wasted += (int)(buffer->end - buffer->write); + total += (int)(chunk->end - chunk->data); + wasted += (int)(chunk->end - chunk->write); ++count; #endif - free(buffer); - buffer = next; + free(chunk); + chunk = next; } #ifdef DEBUG_BUFFERS @@ -765,47 +844,50 @@ void rc_buf_destroy(rc_api_buffer_t* buffer) { } char* rc_buf_reserve(rc_api_buffer_t* buffer, size_t amount) { + rc_api_buffer_chunk_t* chunk = &buffer->chunk; size_t remaining; - while (buffer) { - remaining = buffer->end - buffer->write; + while (chunk) { + remaining = chunk->end - chunk->write; if (remaining >= amount) - return buffer->write; + return chunk->write; - if (!buffer->next) { - /* allocate a chunk of memory that is a multiple of 256-bytes. casting it to an rc_api_buffer_t will - * effectively unbound the data field, so use write and end pointers to track how data is being used. + if (!chunk->next) { + /* allocate a chunk of memory that is a multiple of 256-bytes. the first 32 bytes will be associated + * to the chunk header, and the remaining will be used for data. */ - const size_t buffer_prefix_size = sizeof(rc_api_buffer_t) - sizeof(buffer->data); - const size_t alloc_size = (amount + buffer_prefix_size + 0xFF) & ~0xFF; - buffer->next = (rc_api_buffer_t*)malloc(alloc_size); - if (!buffer->next) + const size_t chunk_header_size = sizeof(rc_api_buffer_chunk_t); + const size_t alloc_size = (chunk_header_size + amount + 0xFF) & ~0xFF; + chunk->next = (rc_api_buffer_chunk_t*)malloc(alloc_size); + if (!chunk->next) break; - buffer->next->write = buffer->next->data; - buffer->next->end = buffer->next->write + (alloc_size - buffer_prefix_size); - buffer->next->next = NULL; + chunk->next->start = (char*)chunk->next + chunk_header_size; + chunk->next->write = chunk->next->start; + chunk->next->end = (char*)chunk->next + alloc_size; + chunk->next->next = NULL; } - buffer = buffer->next; + chunk = chunk->next; } return NULL; } void rc_buf_consume(rc_api_buffer_t* buffer, const char* start, char* end) { + rc_api_buffer_chunk_t* chunk = &buffer->chunk; do { - if (buffer->write == start) { - size_t offset = (end - buffer->data); + if (chunk->write == start) { + size_t offset = (end - chunk->start); offset = (offset + 7) & ~7; - buffer->write = &buffer->data[offset]; + chunk->write = &chunk->start[offset]; - if (buffer->write > buffer->end) - buffer->write = buffer->end; + if (chunk->write > chunk->end) + chunk->write = chunk->end; break; } - buffer = buffer->next; - } while (buffer); + chunk = chunk->next; + } while (chunk); } void* rc_buf_alloc(rc_api_buffer_t* buffer, size_t amount) { @@ -814,6 +896,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); } @@ -828,13 +922,13 @@ void rc_api_format_md5(char checksum[33], const unsigned char digest[16]) { /* --- rc_url_builder --- */ void rc_url_builder_init(rc_api_url_builder_t* builder, rc_api_buffer_t* buffer, size_t estimated_size) { - rc_api_buffer_t* used_buffer; + rc_api_buffer_chunk_t* used_buffer; memset(builder, 0, sizeof(*builder)); builder->buffer = buffer; builder->write = builder->start = rc_buf_reserve(buffer, estimated_size); - used_buffer = buffer; + used_buffer = &buffer->chunk; while (used_buffer && used_buffer->write != builder->write) used_buffer = used_buffer->next; @@ -857,7 +951,7 @@ static int rc_url_builder_reserve(rc_api_url_builder_t* builder, size_t amount) if (remaining < amount) { const size_t used = builder->write - builder->start; const size_t current_size = builder->end - builder->start; - const size_t buffer_prefix_size = sizeof(rc_api_buffer_t) - sizeof(builder->buffer->data); + const size_t buffer_prefix_size = sizeof(rc_api_buffer_chunk_t); char* new_start; size_t new_size = (current_size < 256) ? 256 : current_size * 2; do { @@ -1054,6 +1148,16 @@ static void rc_api_update_host(char** host, const char* hostname) { void rc_api_set_host(const char* hostname) { rc_api_update_host(&g_host, hostname); + + if (!hostname) { + /* also clear out the image hostname */ + rc_api_set_image_host(NULL); + } + else if (strcmp(hostname, RETROACHIEVEMENTS_HOST_NONSSL) == 0) { + /* if just pointing at the non-HTTPS host, explicitly use the default image host + * so it doesn't try to use the web host directly */ + rc_api_set_image_host(RETROACHIEVEMENTS_IMAGE_HOST_NONSSL); + } } void rc_api_set_image_host(const char* hostname) { diff --git a/dep/rcheevos/src/rapi/rc_api_common.h b/dep/rcheevos/src/rapi/rc_api_common.h index 9e2dc53b0..99b17c6d6 100644 --- a/dep/rcheevos/src/rapi/rc_api_common.h +++ b/dep/rcheevos/src/rapi/rc_api_common.h @@ -10,10 +10,13 @@ extern "C" { #endif +#define RC_CONTENT_TYPE_URLENCODED "application/x-www-form-urlencoded" + typedef struct rc_api_url_builder_t { char* write; char* start; char* end; + /* pointer to a preallocated rc_api_buffer_t */ rc_api_buffer_t* buffer; int result; } @@ -23,22 +26,24 @@ void rc_url_builder_init(rc_api_url_builder_t* builder, rc_api_buffer_t* buffer, void rc_url_builder_append(rc_api_url_builder_t* builder, const char* data, size_t len); const char* rc_url_builder_finalize(rc_api_url_builder_t* builder); +#define RC_JSON_NEW_FIELD(n) {NULL,NULL,n,sizeof(n)-1,0} + typedef struct rc_json_field_t { - const char* name; const char* value_start; const char* value_end; + const char* name; + size_t name_len; unsigned array_size; } rc_json_field_t; -typedef struct rc_json_object_field_iterator_t { - rc_json_field_t field; +typedef struct rc_json_iterator_t { const char* json; - size_t name_len; + const char* end; } -rc_json_object_field_iterator_t; +rc_json_iterator_t; -int rc_json_parse_response(rc_api_response_t* response, const char* json, rc_json_field_t* fields, size_t field_count); +int rc_json_parse_server_response(rc_api_response_t* response, const rc_api_server_response_t* server_response, rc_json_field_t* fields, size_t field_count); int rc_json_get_string(const char** out, rc_api_buffer_t* buffer, const rc_json_field_t* field, const char* field_name); int rc_json_get_num(int* out, const rc_json_field_t* field, const char* field_name); int rc_json_get_unum(unsigned* out, const rc_json_field_t* field, const char* field_name); @@ -55,15 +60,18 @@ int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_js int rc_json_get_required_datetime(time_t* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_object(rc_json_field_t* fields, size_t field_count, rc_api_response_t* response, rc_json_field_t* field, const char* field_name); int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); -int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); -int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_field_t* iterator); -int rc_json_get_next_object_field(rc_json_object_field_iterator_t* iterator); +int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); +int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator); +int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field); +int rc_json_get_object_string_length(const char* json); void rc_buf_init(rc_api_buffer_t* buffer); 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/dep/rcheevos/src/rapi/rc_api_editor.c b/dep/rcheevos/src/rapi/rc_api_editor.c index 3d2bd7401..ea4cbe19b 100644 --- a/dep/rcheevos/src/rapi/rc_api_editor.c +++ b/dep/rcheevos/src/rapi/rc_api_editor.c @@ -22,12 +22,24 @@ int rc_api_init_fetch_code_notes_request(rc_api_request_t* request, const rc_api rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* response, const char* server_response) { - rc_json_field_t iterator; + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_code_notes_server_response(response, &response_obj); +} + +int rc_api_process_fetch_code_notes_server_response(rc_api_fetch_code_notes_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t array_field; + rc_json_iterator_t iterator; rc_api_code_note_t* note; const char* address_str; const char* last_author = ""; @@ -36,25 +48,25 @@ int rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"CodeNotes"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("CodeNotes") }; rc_json_field_t note_fields[] = { - {"Address"}, - {"User"}, - {"Note"} + RC_JSON_NEW_FIELD("Address"), + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Note") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; - if (!rc_json_get_required_array(&response->num_notes, &iterator, &response->response, &fields[2], "CodeNotes")) + if (!rc_json_get_required_array(&response->num_notes, &array_field, &response->response, &fields[2], "CodeNotes")) return RC_MISSING_VALUE; if (response->num_notes) { @@ -62,15 +74,21 @@ int rc_api_process_fetch_code_notes_response(rc_api_fetch_code_notes_response_t* if (!response->notes) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + note = response->notes; while (rc_json_get_array_entry_object(note_fields, sizeof(note_fields) / sizeof(note_fields[0]), &iterator)) { /* an empty note represents a record that was deleted on the server */ /* a note set to '' also represents a deleted note (remnant of a bug) */ /* NOTE: len will include the quotes */ - len = note_fields[2].value_end - note_fields[2].value_start; - if (len == 2 || (len == 4 && note_fields[2].value_start[1] == '\'' && note_fields[2].value_start[2] == '\'')) { - --response->num_notes; - continue; + if (note_fields[2].value_start) { + len = note_fields[2].value_end - note_fields[2].value_start; + if (len == 2 || (len == 4 && note_fields[2].value_start[1] == '\'' && note_fields[2].value_start[2] == '\'')) { + --response->num_notes; + continue; + } } if (!rc_json_get_required_string(&address_str, &response->response, ¬e_fields[0], "Address")) @@ -123,27 +141,38 @@ int rc_api_init_update_code_note_request(rc_api_request_t* request, const rc_api rc_url_builder_append_str_param(&builder, "n", api_params->note); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_update_code_note_response(rc_api_update_code_note_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_update_code_note_server_response(response, &response_obj); +} + +int rc_api_process_update_code_note_server_response(rc_api_update_code_note_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error") /* unused fields - {"GameID"}, - {"Address"}, - {"Note"} + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("Address"), + RC_JSON_NEW_FIELD("Note") */ }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -206,23 +235,34 @@ int rc_api_init_update_achievement_request(rc_api_request_t* request, const rc_a rc_url_builder_append_str_param(&builder, "h", buffer); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_update_achievement_response(rc_api_update_achievement_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_update_achievement_server_response(response, &response_obj); +} + +int rc_api_process_update_achievement_server_response(rc_api_update_achievement_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"AchievementID"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("AchievementID") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -297,23 +337,34 @@ int rc_api_init_update_leaderboard_request(rc_api_request_t* request, const rc_a rc_url_builder_append_str_param(&builder, "h", buffer); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_update_leaderboard_response(rc_api_update_leaderboard_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_update_leaderboard_server_response(response, &response_obj); +} + +int rc_api_process_update_leaderboard_server_response(rc_api_update_leaderboard_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"LeaderboardID"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("LeaderboardID") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -338,24 +389,37 @@ int rc_api_init_fetch_badge_range_request(rc_api_request_t* request, const rc_ap rc_url_builder_append_str_param(&builder, "r", "badgeiter"); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + (void)api_params; return builder.result; } int rc_api_process_fetch_badge_range_response(rc_api_fetch_badge_range_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_badge_range_server_response(response, &response_obj); +} + +int rc_api_process_fetch_badge_range_server_response(rc_api_fetch_badge_range_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"FirstBadge"}, - {"NextBadge"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("FirstBadge"), + RC_JSON_NEW_FIELD("NextBadge") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -399,33 +463,44 @@ int rc_api_init_add_game_hash_request(rc_api_request_t* request, const rc_api_ad rc_url_builder_append_str_param(&builder, "d", api_params->hash_description); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_add_game_hash_response(rc_api_add_game_hash_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_add_game_hash_server_response(response, &response_obj); +} + +int rc_api_process_add_game_hash_server_response(rc_api_add_game_hash_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"Response"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") }; rc_json_field_t response_fields[] = { - {"GameID"} + RC_JSON_NEW_FIELD("GameID") /* unused fields - {"MD5"}, - {"ConsoleID"}, - {"GameTitle"}, - {"Success"} + RC_JSON_NEW_FIELD("MD5"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("GameTitle"), + RC_JSON_NEW_FIELD("Success") */ }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; diff --git a/dep/rcheevos/src/rapi/rc_api_info.c b/dep/rcheevos/src/rapi/rc_api_info.c index 7dc758a1c..0ee4de79c 100644 --- a/dep/rcheevos/src/rapi/rc_api_info.c +++ b/dep/rcheevos/src/rapi/rc_api_info.c @@ -27,45 +27,57 @@ int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_url_builder_append_unum_param(&builder, "c", api_params->count); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_achievement_info_server_response(response, &response_obj); +} + +int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achievement_info_response_t* response, const rc_api_server_response_t* server_response) { rc_api_achievement_awarded_entry_t* entry; - rc_json_field_t iterator; + rc_json_field_t array_field; + rc_json_iterator_t iterator; unsigned timet; int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"AchievementID"}, - {"Response"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("AchievementID"), + RC_JSON_NEW_FIELD("Response") /* unused fields - {"Offset"}, - {"Count"}, - {"FriendsOnly"}, + RC_JSON_NEW_FIELD("Offset"), + RC_JSON_NEW_FIELD("Count"), + RC_JSON_NEW_FIELD("FriendsOnly") * unused fields */ }; rc_json_field_t response_fields[] = { - {"NumEarned"}, - {"TotalPlayers"}, - {"GameID"}, - {"RecentWinner"} /* array */ + RC_JSON_NEW_FIELD("NumEarned"), + RC_JSON_NEW_FIELD("TotalPlayers"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("RecentWinner") /* array */ }; rc_json_field_t entry_fields[] = { - {"User"}, - {"DateAwarded"} + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("DateAwarded") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -81,7 +93,7 @@ int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info if (!rc_json_get_required_unum(&response->game_id, &response->response, &response_fields[2], "GameID")) return RC_MISSING_VALUE; - if (!rc_json_get_required_array(&response->num_recently_awarded, &iterator, &response->response, &response_fields[3], "RecentWinner")) + if (!rc_json_get_required_array(&response->num_recently_awarded, &array_field, &response->response, &response_fields[3], "RecentWinner")) return RC_MISSING_VALUE; if (response->num_recently_awarded) { @@ -89,6 +101,10 @@ int rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info if (!response->recently_awarded) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + entry = response->recently_awarded; while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) @@ -130,57 +146,69 @@ int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_url_builder_append_unum_param(&builder, "c", api_params->count); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_leaderboard_info_server_response(response, &response_obj); +} + +int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboard_info_response_t* response, const rc_api_server_response_t* server_response) { rc_api_lboard_info_entry_t* entry; - rc_json_field_t iterator; + rc_json_field_t array_field; + rc_json_iterator_t iterator; unsigned timet; int result; size_t len; char format[16]; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"LeaderboardData"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("LeaderboardData") }; rc_json_field_t leaderboarddata_fields[] = { - {"LBID"}, - {"LBFormat"}, - {"LowerIsBetter"}, - {"LBTitle"}, - {"LBDesc"}, - {"LBMem"}, - {"GameID"}, - {"LBAuthor"}, - {"LBCreated"}, - {"LBUpdated"}, - {"Entries"} /* array */ + RC_JSON_NEW_FIELD("LBID"), + RC_JSON_NEW_FIELD("LBFormat"), + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("LBTitle"), + RC_JSON_NEW_FIELD("LBDesc"), + RC_JSON_NEW_FIELD("LBMem"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("LBAuthor"), + RC_JSON_NEW_FIELD("LBCreated"), + RC_JSON_NEW_FIELD("LBUpdated"), + RC_JSON_NEW_FIELD("Entries") /* array */ /* unused fields - {"GameTitle"}, - {"ConsoleID"}, - {"ConsoleName"}, - {"ForumTopicID"}, - {"GameIcon"}, + RC_JSON_NEW_FIELD("GameTitle"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("ConsoleName"), + RC_JSON_NEW_FIELD("ForumTopicID"), + RC_JSON_NEW_FIELD("GameIcon") * unused fields */ }; rc_json_field_t entry_fields[] = { - {"User"}, - {"Rank"}, - {"Index"}, - {"Score"}, - {"DateSubmitted"} + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("Index"), + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("DateSubmitted") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -218,7 +246,7 @@ int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info response->format = RC_FORMAT_VALUE; } - if (!rc_json_get_required_array(&response->num_entries, &iterator, &response->response, &leaderboarddata_fields[10], "Entries")) + if (!rc_json_get_required_array(&response->num_entries, &array_field, &response->response, &leaderboarddata_fields[10], "Entries")) return RC_MISSING_VALUE; if (response->num_entries) { @@ -226,6 +254,10 @@ int rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info if (!response->entries) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + entry = response->entries; while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) @@ -270,26 +302,38 @@ int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api rc_url_builder_append_unum_param(&builder, "c", api_params->console_id); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_games_list_server_response(response, &response_obj); +} + +int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response) { rc_api_game_list_entry_t* entry; - rc_json_object_field_iterator_t iterator; + rc_json_iterator_t iterator; + rc_json_field_t field; int result; char* end; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"Response"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -308,13 +352,14 @@ int rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* memset(&iterator, 0, sizeof(iterator)); iterator.json = fields[2].value_start; + iterator.end = fields[2].value_end; entry = response->entries; - while (rc_json_get_next_object_field(&iterator)) { - entry->id = strtol(iterator.field.name, &end, 10); + while (rc_json_get_next_object_field(&iterator, &field)) { + entry->id = strtol(field.name, &end, 10); - iterator.field.name = ""; - if (!rc_json_get_string(&entry->name, &response->response.buffer, &iterator.field, "")) + field.name = ""; + if (!rc_json_get_string(&entry->name, &response->response.buffer, &field, "")) return RC_MISSING_VALUE; ++entry; diff --git a/dep/rcheevos/src/rapi/rc_api_runtime.c b/dep/rcheevos/src/rapi/rc_api_runtime.c index e42db3cf1..7bac4ec53 100644 --- a/dep/rcheevos/src/rapi/rc_api_runtime.c +++ b/dep/rcheevos/src/rapi/rc_api_runtime.c @@ -24,22 +24,33 @@ int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_res rc_url_builder_append_str_param(&builder, "r", "gameid"); rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_resolve_hash_server_response(response, &response_obj); +} + +int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"GameID"}, + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("GameID") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -65,17 +76,30 @@ int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_ if (rc_api_url_build_dorequest(&builder, "patch", api_params->username, api_params->api_token)) { rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_game_data_server_response(response, &response_obj); +} + +int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response) { rc_api_achievement_definition_t* achievement; rc_api_leaderboard_definition_t* leaderboard; - rc_json_field_t iterator; + rc_json_field_t array_field; + rc_json_iterator_t iterator; const char* str; const char* last_author = ""; + const char* last_author_field = ""; size_t last_author_len = 0; size_t len; unsigned timet; @@ -83,52 +107,52 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r char format[16]; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"PatchData"} /* nested object */ + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("PatchData") /* nested object */ }; rc_json_field_t patchdata_fields[] = { - {"ID"}, - {"Title"}, - {"ConsoleID"}, - {"ImageIcon"}, - {"RichPresencePatch"}, - {"Achievements"}, /* array */ - {"Leaderboards"} /* array */ + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("ImageIcon"), + RC_JSON_NEW_FIELD("RichPresencePatch"), + RC_JSON_NEW_FIELD("Achievements"), /* array */ + RC_JSON_NEW_FIELD("Leaderboards") /* array */ /* unused fields - {"ForumTopicID"}, - {"Flags"}, + RC_JSON_NEW_FIELD("ForumTopicID"), + RC_JSON_NEW_FIELD("Flags") * unused fields */ }; rc_json_field_t achievement_fields[] = { - {"ID"}, - {"Title"}, - {"Description"}, - {"Flags"}, - {"Points"}, - {"MemAddr"}, - {"Author"}, - {"BadgeName"}, - {"Created"}, - {"Modified"} + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("Description"), + RC_JSON_NEW_FIELD("Flags"), + RC_JSON_NEW_FIELD("Points"), + RC_JSON_NEW_FIELD("MemAddr"), + RC_JSON_NEW_FIELD("Author"), + RC_JSON_NEW_FIELD("BadgeName"), + RC_JSON_NEW_FIELD("Created"), + RC_JSON_NEW_FIELD("Modified") }; rc_json_field_t leaderboard_fields[] = { - {"ID"}, - {"Title"}, - {"Description"}, - {"Mem"}, - {"Format"}, - {"LowerIsBetter"}, - {"Hidden"} + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("Description"), + RC_JSON_NEW_FIELD("Mem"), + RC_JSON_NEW_FIELD("Format"), + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("Hidden") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -174,7 +198,7 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r if (!response->rich_presence_script) response->rich_presence_script = ""; - if (!rc_json_get_required_array(&response->num_achievements, &iterator, &response->response, &patchdata_fields[5], "Achievements")) + if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[5], "Achievements")) return RC_MISSING_VALUE; if (response->num_achievements) { @@ -182,6 +206,10 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r if (!response->achievements) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + achievement = response->achievements; while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) { if (!rc_json_get_required_unum(&achievement->id, &response->response, &achievement_fields[0], "ID")) @@ -199,8 +227,8 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r if (!rc_json_get_required_string(&achievement->badge_name, &response->response, &achievement_fields[7], "BadgeName")) return RC_MISSING_VALUE; - len = achievement_fields[7].value_end - achievement_fields[7].value_start; - if (len == last_author_len && memcmp(achievement_fields[7].value_start, last_author, len) == 0) { + len = achievement_fields[6].value_end - achievement_fields[6].value_start; + if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) { achievement->author = last_author; } else { @@ -208,6 +236,7 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r return RC_MISSING_VALUE; last_author = achievement->author; + last_author_field = achievement_fields[6].value_start; last_author_len = len; } @@ -222,7 +251,7 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r } } - if (!rc_json_get_required_array(&response->num_leaderboards, &iterator, &response->response, &patchdata_fields[6], "Leaderboards")) + if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[6], "Leaderboards")) return RC_MISSING_VALUE; if (response->num_leaderboards) { @@ -230,6 +259,10 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r if (!response->leaderboards) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + leaderboard = response->leaderboards; while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) { if (!rc_json_get_required_unum(&leaderboard->id, &response->response, &leaderboard_fields[0], "ID")) @@ -284,21 +317,32 @@ int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_reques rc_url_builder_append_str_param(&builder, "m", api_params->rich_presence); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_ping_server_response(response, &response_obj); +} + +int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response) { rc_json_field_t fields[] = { - {"Success"}, - {"Error"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - return rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); } void rc_api_destroy_ping_response(rc_api_ping_response_t* response) { @@ -337,25 +381,37 @@ int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_ap rc_url_builder_append_str_param(&builder, "v", buffer); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_award_achievement_server_response(response, &response_obj); +} + +int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"Score"}, - {"AchievementID"}, - {"AchievementsRemaining"} + 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") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK) return result; @@ -373,8 +429,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; } @@ -416,67 +473,79 @@ int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_ rc_url_builder_append_str_param(&builder, "v", buffer); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_submit_lboard_entry_server_response(response, &response_obj); +} + +int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response) { rc_api_lboard_entry_t* entry; - rc_json_field_t iterator; + rc_json_field_t array_field; + rc_json_iterator_t iterator; const char* str; int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"Response"} /* nested object */ + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response") /* nested object */ }; rc_json_field_t response_fields[] = { - {"Score"}, - {"BestScore"}, - {"RankInfo"}, /* nested object */ - {"TopEntries"} /* array */ + RC_JSON_NEW_FIELD("Score"), + RC_JSON_NEW_FIELD("BestScore"), + RC_JSON_NEW_FIELD("RankInfo"), /* nested object */ + RC_JSON_NEW_FIELD("TopEntries") /* array */ /* unused fields - {"LBData"}, / * array * / - {"ScoreFormatted"}, - {"TopEntriesFriends"}, / * array * / + RC_JSON_NEW_FIELD("LBData"), / * array * / + RC_JSON_NEW_FIELD("ScoreFormatted"), + RC_JSON_NEW_FIELD("TopEntriesFriends") / * array * / * unused fields */ }; /* unused fields rc_json_field_t lbdata_fields[] = { - {"Format"}, - {"LeaderboardID"}, - {"GameID"}, - {"Title"}, - {"LowerIsBetter"} + RC_JSON_NEW_FIELD("Format"), + RC_JSON_NEW_FIELD("LeaderboardID"), + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("LowerIsBetter") }; * unused fields */ rc_json_field_t entry_fields[] = { - {"User"}, - {"Rank"}, - {"Score"} + RC_JSON_NEW_FIELD("User"), + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("Score") /* unused fields - {"DateSubmitted"}, + RC_JSON_NEW_FIELD("DateSubmitted") * unused fields */ }; rc_json_field_t rank_info_fields[] = { - {"Rank"}, - {"NumEntries"} + RC_JSON_NEW_FIELD("Rank"), + RC_JSON_NEW_FIELD("NumEntries") /* unused fields - {"LowerIsBetter"}, - {"UserRank"}, + RC_JSON_NEW_FIELD("LowerIsBetter"), + RC_JSON_NEW_FIELD("UserRank") * unused fields */ }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -495,7 +564,7 @@ int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_respo return RC_MISSING_VALUE; response->num_entries = (unsigned)atoi(str); - if (!rc_json_get_required_array(&response->num_top_entries, &iterator, &response->response, &response_fields[3], "TopEntries")) + if (!rc_json_get_required_array(&response->num_top_entries, &array_field, &response->response, &response_fields[3], "TopEntries")) return RC_MISSING_VALUE; if (response->num_top_entries) { @@ -503,6 +572,10 @@ int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_respo if (!response->top_entries) return RC_OUT_OF_MEMORY; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + entry = response->top_entries; while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User")) diff --git a/dep/rcheevos/src/rapi/rc_api_user.c b/dep/rcheevos/src/rapi/rc_api_user.c index e5181ba95..4a13b8df2 100644 --- a/dep/rcheevos/src/rapi/rc_api_user.c +++ b/dep/rcheevos/src/rapi/rc_api_user.c @@ -1,6 +1,8 @@ #include "rc_api_user.h" #include "rc_api_common.h" +#include "../rcheevos/rc_version.h" + #include /* --- Login --- */ @@ -25,26 +27,38 @@ int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_requ return RC_INVALID_STATE; request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; return builder.result; } int rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_login_server_response(response, &response_obj); +} + +int rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"User"}, - {"Token"}, - {"Score"}, - {"Messages"}, - {"DisplayName"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + 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") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; @@ -54,9 +68,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; } @@ -86,22 +101,34 @@ int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_st */ rc_url_builder_append_unum_param(&builder, "a", 3); rc_url_builder_append_unum_param(&builder, "m", api_params->game_id); + rc_url_builder_append_str_param(&builder, "l", RCHEEVOS_VERSION_STRING); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_start_session_server_response(response, &response_obj); +} + +int rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response) { rc_json_field_t fields[] = { - {"Success"}, - {"Error"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error") }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - return rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); } void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response) { @@ -120,27 +147,38 @@ int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_a rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0); request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; } return builder.result; } int rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response) { + rc_api_server_response_t response_obj; + + memset(&response_obj, 0, sizeof(response_obj)); + response_obj.body = server_response; + response_obj.body_length = rc_json_get_object_string_length(server_response); + + return rc_api_process_fetch_user_unlocks_server_response(response, &response_obj); +} + +int rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response) { int result; rc_json_field_t fields[] = { - {"Success"}, - {"Error"}, - {"UserUnlocks"} + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("UserUnlocks") /* unused fields - { "GameID" }, - { "HardcoreMode" } + RC_JSON_NEW_FIELD("GameID"), + RC_JSON_NEW_FIELD("HardcoreMode") * unused fields */ }; memset(response, 0, sizeof(*response)); rc_buf_init(&response->response.buffer); - result = rc_json_parse_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); if (result != RC_OK || !response->response.succeeded) return result; diff --git a/dep/rcheevos/src/rcheevos/alloc.c b/dep/rcheevos/src/rcheevos/alloc.c index c361eecee..ac2a07bc0 100644 --- a/dep/rcheevos/src/rcheevos/alloc.c +++ b/dep/rcheevos/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/dep/rcheevos/src/rcheevos/compat.c b/dep/rcheevos/src/rcheevos/compat.c index 877c64ddb..0ba7601ad 100644 --- a/dep/rcheevos/src/rcheevos/compat.c +++ b/dep/rcheevos/src/rcheevos/compat.c @@ -3,6 +3,8 @@ #include #include +#ifdef RC_C89_HELPERS + int rc_strncasecmp(const char* left, const char* right, size_t length) { while (length) @@ -44,20 +46,94 @@ char* rc_strdup(const char* str) { const size_t length = strlen(str); char* buffer = (char*)malloc(length + 1); - memcpy(buffer, str, length + 1); + if (buffer) + memcpy(buffer, str, length + 1); return buffer; } int rc_snprintf(char* buffer, size_t size, const char* format, ...) { - int result; - va_list args; + int result; + va_list args; - va_start(args, format); - /* assume buffer is large enough and ignore size */ - (void)size; - result = vsprintf(buffer, format, args); - va_end(args); + va_start(args, format); - return result; +#ifdef __STDC_WANT_SECURE_LIB__ + result = vsprintf_s(buffer, size, format, args); +#else + /* assume buffer is large enough and ignore size */ + (void)size; + result = vsprintf(buffer, format, args); +#endif + + va_end(args); + + return result; } + +#endif + +#ifndef __STDC_WANT_SECURE_LIB__ + +struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer) +{ + struct tm* tm = gmtime(timer); + memcpy(buf, tm, sizeof(*tm)); + return buf; +} + +#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/dep/rcheevos/src/rcheevos/condition.c b/dep/rcheevos/src/rcheevos/condition.c index 03aa0484e..55a8084e7 100644 --- a/dep/rcheevos/src/rcheevos/condition.c +++ b/dep/rcheevos/src/rcheevos/condition.c @@ -1,6 +1,91 @@ #include "rc_internal.h" #include +#include + +static int rc_test_condition_compare(unsigned value1, unsigned value2, char oper) { + switch (oper) { + case RC_OPERATOR_EQ: return value1 == value2; + case RC_OPERATOR_NE: return value1 != value2; + case RC_OPERATOR_LT: return value1 < value2; + case RC_OPERATOR_LE: return value1 <= value2; + case RC_OPERATOR_GT: return value1 > value2; + case RC_OPERATOR_GE: return value1 >= value2; + default: return 1; + } +} + +static char rc_condition_determine_comparator(const rc_condition_t* self) { + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_NE: + case RC_OPERATOR_LT: + case RC_OPERATOR_LE: + case RC_OPERATOR_GT: + case RC_OPERATOR_GE: + break; + + default: + /* not a comparison. should not be getting compared. but if it is, legacy behavior was to return 1 */ + return RC_PROCESSING_COMPARE_ALWAYS_TRUE; + } + + if ((self->operand1.type == RC_OPERAND_ADDRESS || self->operand1.type == RC_OPERAND_DELTA) && + !self->operand1.value.memref->value.is_indirect && !rc_operand_is_float(&self->operand1)) { + /* left side is an integer memory reference */ + int needs_translate = (self->operand1.size != self->operand1.value.memref->value.size); + + if (self->operand2.type == RC_OPERAND_CONST) { + /* right side is a constant */ + if (self->operand1.type == RC_OPERAND_ADDRESS) + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_CONST; + + return needs_translate ? RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED : RC_PROCESSING_COMPARE_DELTA_TO_CONST; + } + else if ((self->operand2.type == RC_OPERAND_ADDRESS || self->operand2.type == RC_OPERAND_DELTA) && + !self->operand2.value.memref->value.is_indirect && !rc_operand_is_float(&self->operand2)) { + /* right side is an integer memory reference */ + const int is_same_memref = (self->operand1.value.memref == self->operand2.value.memref); + needs_translate |= (self->operand2.size != self->operand2.value.memref->value.size); + + if (self->operand1.type == RC_OPERAND_ADDRESS) { + if (self->operand2.type == RC_OPERAND_ADDRESS) { + if (is_same_memref && !needs_translate) { + /* comparing a memref to itself, will evaluate to a constant */ + return rc_test_condition_compare(0, 0, self->oper) ? RC_PROCESSING_COMPARE_ALWAYS_TRUE : RC_PROCESSING_COMPARE_ALWAYS_FALSE; + } + + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF; + } + + assert(self->operand2.type == RC_OPERAND_DELTA); + + if (is_same_memref) { + /* delta comparison is optimized to compare with itself (for detecting change) */ + return needs_translate ? RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED : RC_PROCESSING_COMPARE_MEMREF_TO_DELTA; + } + } + else { + assert(self->operand1.type == RC_OPERAND_DELTA); + + if (self->operand2.type == RC_OPERAND_ADDRESS) { + if (is_same_memref) { + /* delta comparison is optimized to compare with itself (for detecting change) */ + return needs_translate ? RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED : RC_PROCESSING_COMPARE_DELTA_TO_MEMREF; + } + } + } + } + } + + if (self->operand1.type == RC_OPERAND_CONST && self->operand2.type == RC_OPERAND_CONST) { + /* comparing constants will always generate a constant result */ + return rc_test_condition_compare(self->operand1.value.num, self->operand2.value.num, self->oper) ? + RC_PROCESSING_COMPARE_ALWAYS_TRUE : RC_PROCESSING_COMPARE_ALWAYS_FALSE; + } + + return RC_PROCESSING_COMPARE_DEFAULT; +} static int rc_parse_operator(const char** memaddr) { const char* oper = *memaddr; @@ -50,6 +135,10 @@ static int rc_parse_operator(const char** memaddr) { ++(*memaddr); return RC_OPERATOR_AND; + case '^': + ++(*memaddr); + return RC_OPERATOR_XOR; + case '\0':/* end of string */ case '_': /* next condition */ case 'S': /* next condset */ @@ -71,6 +160,7 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse self->current_hits = 0; self->is_true = 0; self->pause = 0; + self->optimized_comparator = RC_PROCESSING_COMPARE_DEFAULT; if (*aux != 0 && aux[1] == ':') { switch (*aux) { @@ -135,6 +225,7 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse case RC_OPERATOR_MULT: case RC_OPERATOR_DIV: case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: /* modifying operators are only valid on modifying statements */ if (can_modify) break; @@ -209,6 +300,9 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse self->required_hits = 0; } + if (parse->buffer != 0) + self->optimized_comparator = rc_condition_determine_comparator(self); + *memaddr = aux; return self; } @@ -228,12 +322,203 @@ int rc_condition_is_combining(const rc_condition_t* self) { } } +static int rc_test_condition_compare_memref_to_const(rc_condition_t* self) { + const unsigned value1 = self->operand1.value.memref->value.value; + const unsigned value2 = self->operand2.value.num; + assert(self->operand1.size == self->operand1.value.memref->value.size); + return rc_test_condition_compare(value1, value2, self->oper); +} + +static int rc_test_condition_compare_delta_to_const(rc_condition_t* self) { + const rc_memref_value_t* memref1 = &self->operand1.value.memref->value; + const unsigned value1 = (memref1->changed) ? memref1->prior : memref1->value; + const unsigned value2 = self->operand2.value.num; + assert(self->operand1.size == self->operand1.value.memref->value.size); + return rc_test_condition_compare(value1, value2, self->oper); +} + +static int rc_test_condition_compare_memref_to_memref(rc_condition_t* self) { + const unsigned value1 = self->operand1.value.memref->value.value; + const unsigned value2 = self->operand2.value.memref->value.value; + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + return rc_test_condition_compare(value1, value2, self->oper); +} + +static int rc_test_condition_compare_memref_to_delta(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + + if (memref->changed) + return rc_test_condition_compare(memref->value, memref->prior, self->oper); + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_delta_to_memref(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + assert(self->operand1.size == self->operand1.value.memref->value.size); + assert(self->operand2.size == self->operand2.value.memref->value.size); + + if (memref->changed) + return rc_test_condition_compare(memref->prior, memref->value, self->oper); + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_memref_to_const_transformed(rc_condition_t* self) { + rc_typed_value_t value1; + const unsigned value2 = self->operand2.value.num; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = self->operand1.value.memref->value.value; + rc_transform_memref_value(&value1, self->operand1.size); + + return rc_test_condition_compare(value1.value.u32, value2, self->oper); +} + +static int rc_test_condition_compare_delta_to_const_transformed(rc_condition_t* self) { + rc_typed_value_t value1; + const rc_memref_value_t* memref1 = &self->operand1.value.memref->value; + const unsigned value2 = self->operand2.value.num; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = (memref1->changed) ? memref1->prior : memref1->value; + rc_transform_memref_value(&value1, self->operand1.size); + + return rc_test_condition_compare(value1.value.u32, value2, self->oper); +} + +static int rc_test_condition_compare_memref_to_memref_transformed(rc_condition_t* self) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = self->operand1.value.memref->value.value; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = self->operand2.value.memref->value.value; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); +} + +static int rc_test_condition_compare_memref_to_delta_transformed(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + + if (memref->changed) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = memref->value; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = memref->prior; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); + } + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + +static int rc_test_condition_compare_delta_to_memref_transformed(rc_condition_t* self) { + const rc_memref_value_t* memref = &self->operand1.value.memref->value; + assert(self->operand1.value.memref == self->operand2.value.memref); + + if (memref->changed) { + rc_typed_value_t value1, value2; + + value1.type = RC_VALUE_TYPE_UNSIGNED; + value1.value.u32 = memref->prior; + rc_transform_memref_value(&value1, self->operand1.size); + + value2.type = RC_VALUE_TYPE_UNSIGNED; + value2.value.u32 = memref->value; + rc_transform_memref_value(&value2, self->operand2.size); + + return rc_test_condition_compare(value1.value.u32, value2.value.u32, self->oper); + } + + switch (self->oper) { + case RC_OPERATOR_EQ: + case RC_OPERATOR_GE: + case RC_OPERATOR_LE: + return 1; + + default: + return 0; + } +} + int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state) { rc_typed_value_t value1, value2; - rc_evaluate_operand(&value1, &self->operand1, eval_state); - if (eval_state->add_value.type != RC_VALUE_TYPE_NONE) + if (eval_state->add_value.type != RC_VALUE_TYPE_NONE) { + /* if there's an accumulator, we can't use the optimized comparators */ + rc_evaluate_operand(&value1, &self->operand1, eval_state); rc_typed_value_add(&value1, &eval_state->add_value); + } else { + /* use an optimized comparator whenever possible */ + switch (self->optimized_comparator) { + case RC_PROCESSING_COMPARE_MEMREF_TO_CONST: + return rc_test_condition_compare_memref_to_const(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_DELTA: + return rc_test_condition_compare_memref_to_delta(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF: + return rc_test_condition_compare_memref_to_memref(self); + case RC_PROCESSING_COMPARE_DELTA_TO_CONST: + return rc_test_condition_compare_delta_to_const(self); + case RC_PROCESSING_COMPARE_DELTA_TO_MEMREF: + return rc_test_condition_compare_delta_to_memref(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED: + return rc_test_condition_compare_memref_to_const_transformed(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED: + return rc_test_condition_compare_memref_to_delta_transformed(self); + case RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED: + return rc_test_condition_compare_memref_to_memref_transformed(self); + case RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED: + return rc_test_condition_compare_delta_to_const_transformed(self); + case RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED: + return rc_test_condition_compare_delta_to_memref_transformed(self); + case RC_PROCESSING_COMPARE_ALWAYS_TRUE: + return 1; + case RC_PROCESSING_COMPARE_ALWAYS_FALSE: + return 0; + default: + rc_evaluate_operand(&value1, &self->operand1, eval_state); + break; + } + } rc_evaluate_operand(&value2, &self->operand2, eval_state); @@ -260,5 +545,11 @@ void rc_evaluate_condition_value(rc_typed_value_t* value, rc_condition_t* self, rc_typed_value_convert(&amount, RC_VALUE_TYPE_UNSIGNED); value->value.u32 &= amount.value.u32; break; + + case RC_OPERATOR_XOR: + rc_typed_value_convert(value, RC_VALUE_TYPE_UNSIGNED); + rc_typed_value_convert(&amount, RC_VALUE_TYPE_UNSIGNED); + value->value.u32 ^= amount.value.u32; + break; } } diff --git a/dep/rcheevos/src/rcheevos/condset.c b/dep/rcheevos/src/rcheevos/condset.c index c7c7e6dd6..e77a23581 100644 --- a/dep/rcheevos/src/rcheevos/condset.c +++ b/dep/rcheevos/src/rcheevos/condset.c @@ -84,6 +84,7 @@ rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, in switch ((*next)->oper) { case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: case RC_OPERATOR_DIV: case RC_OPERATOR_MULT: case RC_OPERATOR_NONE: @@ -209,8 +210,7 @@ static int rc_test_condset_internal(rc_condset_t* self, int processing_pause, rc case RC_CONDITION_SUB_SOURCE: rc_evaluate_condition_value(&value, condition, eval_state); - rc_typed_value_convert(&value, RC_VALUE_TYPE_SIGNED); - value.value.i32 = -value.value.i32; + rc_typed_value_negate(&value); rc_typed_value_add(&eval_state->add_value, &value); eval_state->add_address = 0; continue; diff --git a/dep/rcheevos/src/rcheevos/consoleinfo.c b/dep/rcheevos/src/rcheevos/consoleinfo.c index 9f2665d7e..0ef0089e8 100644 --- a/dep/rcheevos/src/rcheevos/consoleinfo.c +++ b/dep/rcheevos/src/rcheevos/consoleinfo.c @@ -39,6 +39,9 @@ const char* rc_console_name(int console_id) case RC_CONSOLE_ATARI_JAGUAR: return "Atari Jaguar"; + case RC_CONSOLE_ATARI_JAGUAR_CD: + return "Atari Jaguar CD"; + case RC_CONSOLE_ATARI_LYNX: return "Atari Lynx"; @@ -132,6 +135,9 @@ const char* rc_console_name(int console_id) case RC_CONSOLE_NINTENDO_DS: return "Nintendo DS"; + case RC_CONSOLE_NINTENDO_DSI: + return "Nintendo DSi"; + case RC_CONSOLE_NINTENDO_3DS: return "Nintendo 3DS"; @@ -156,6 +162,9 @@ const char* rc_console_name(int console_id) case RC_CONSOLE_PC_ENGINE: return "PC Engine"; + case RC_CONSOLE_PC_ENGINE_CD: + return "PC Engine CD"; + case RC_CONSOLE_PLAYSTATION: return "PlayStation"; @@ -198,9 +207,15 @@ const char* rc_console_name(int console_id) case RC_CONSOLE_THOMSONTO8: return "Thomson TO8"; + case RC_CONSOLE_TI83: + return "TI-83"; + case RC_CONSOLE_TIC80: return "TIC-80"; + case RC_CONSOLE_UZEBOX: + return "Uzebox"; + case RC_CONSOLE_VECTREX: return "Vectrex"; @@ -435,6 +450,13 @@ static const rc_memory_region_t _rc_memory_regions_gameboy_advance[] = { }; static const rc_memory_regions_t rc_memory_regions_gameboy_advance = { _rc_memory_regions_gameboy_advance, 2 }; +/* ===== GameCube ===== */ +/* https://wiibrew.org/wiki/Memory_map */ +static const rc_memory_region_t _rc_memory_regions_gamecube[] = { + { 0x00000000U, 0x017FFFFF, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_gamecube = { _rc_memory_regions_gamecube, 1 }; + /* ===== Game Gear ===== */ /* http://www.smspower.org/Development/MemoryMap */ static const rc_memory_region_t _rc_memory_regions_game_gear[] = { @@ -518,6 +540,15 @@ static const rc_memory_region_t _rc_memory_regions_megadrive[] = { }; static const rc_memory_regions_t rc_memory_regions_megadrive = { _rc_memory_regions_megadrive, 2 }; +/* ===== MegaDrive 32X (Genesis 32X) ===== */ +/* http://devster.monkeeh.com/sega/32xguide1.txt */ +static const rc_memory_region_t _rc_memory_regions_megadrive_32x[] = { + { 0x000000U, 0x00FFFFU, 0x00FF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Main MegaDrive RAM */ + { 0x010000U, 0x04FFFFU, 0x06000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "32X RAM"}, /* Additional 32X RAM */ + { 0x050000U, 0x05FFFFU, 0x00000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_megadrive_32x = { _rc_memory_regions_megadrive_32x, 3 }; + /* ===== MSX ===== */ /* https://www.msx.org/wiki/The_Memory */ /* MSX only has 64KB of addressable RAM, of which 32KB is reserved for the system/BIOS. @@ -540,6 +571,35 @@ static const rc_memory_region_t _rc_memory_regions_neo_geo_pocket[] = { }; static const rc_memory_regions_t rc_memory_regions_neo_geo_pocket = { _rc_memory_regions_neo_geo_pocket, 1 }; +/* ===== Neo Geo CD ===== */ +/* https://wiki.neogeodev.org/index.php?title=68k_memory_map */ +/* NeoCD exposes $000000-$1FFFFF as System RAM, but it seems like only the WORKRAM section is used. + * This is consistent with http://www.hardmvs.fr/manuals/NeoGeoProgrammersGuide.pdf (page25), which says: + * + * Furthermore, the NEO-GEO provides addresses 100000H-10FFFFH as a work area, out of which the + * addresses 10F300H-10FFFFH are reserved exclusively for use by the system program. Therefore, + * every game is to use addresses 100000H-10F2FFH. + * + * Also note that PRG files (game ROM) can be loaded anywhere else in the $000000-$1FFFFF range. + * AoF3 illustrates this pretty clearly: https://wiki.neogeodev.org/index.php?title=IPL_file + * + * PROG_CD.PRG,0,0 + * PROG_CDX.PRG,0,058000 + * CNV_NM.PRG,0,0C0000 + * FIX_DATA.PRG,0,0FD000 + * OBJACTLK.PRG,0,130000 + * SSEL_CNV.PRG,0,15A000 + * SSEL_BAK.PRG,0,16F000 + * HITMSG.PRG,0,170000 + * SSEL_SPR.PRG,0,19D000 + */ +static const rc_memory_region_t _rc_memory_regions_neo_geo_cd[] = { + { 0x000000U, 0x00F2FFU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* NOTE: some BIOS settings are exposed through the reserved RAM: https://wiki.neogeodev.org/index.php?title=68k_ASM_defines */ + { 0x00F300U, 0x00FFFFU, 0x0010F300U, RC_MEMORY_TYPE_SYSTEM_RAM, "Reserved RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_neo_geo_cd = { _rc_memory_regions_neo_geo_cd, 2 }; + /* ===== Nintendo Entertainment System ===== */ /* https://wiki.nesdev.com/w/index.php/CPU_memory_map */ static const rc_memory_region_t _rc_memory_regions_nes[] = { @@ -587,6 +647,13 @@ static const rc_memory_region_t _rc_memory_regions_nintendo_ds[] = { }; static const rc_memory_regions_t rc_memory_regions_nintendo_ds = { _rc_memory_regions_nintendo_ds, 1 }; +/* ===== Nintendo DSi ===== */ +/* https://problemkaputt.de/gbatek.htm#dsiiomap */ +static const rc_memory_region_t _rc_memory_regions_nintendo_dsi[] = { + { 0x000000U, 0xFFFFFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_nintendo_dsi = { _rc_memory_regions_nintendo_dsi, 1 }; + /* ===== Oric ===== */ static const rc_memory_region_t _rc_memory_regions_oric[] = { /* actual size depends on machine type - up to 64KB */ @@ -605,11 +672,18 @@ static const rc_memory_regions_t rc_memory_regions_pc8800 = { _rc_memory_regions /* http://www.archaicpixels.com/Memory_Map */ static const rc_memory_region_t _rc_memory_regions_pc_engine[] = { { 0x000000U, 0x001FFFU, 0x1F0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_pc_engine = { _rc_memory_regions_pc_engine, 1 }; + +/* ===== PC Engine CD===== */ +/* http://www.archaicpixels.com/Memory_Map */ +static const rc_memory_region_t _rc_memory_regions_pc_engine_cd[] = { + { 0x000000U, 0x001FFFU, 0x1F0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, { 0x002000U, 0x011FFFU, 0x100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "CD RAM" }, { 0x012000U, 0x041FFFU, 0x0D0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Super System Card RAM" }, { 0x042000U, 0x0427FFU, 0x1EE000U, RC_MEMORY_TYPE_SAVE_RAM, "CD Battery-backed RAM" } }; -static const rc_memory_regions_t rc_memory_regions_pc_engine = { _rc_memory_regions_pc_engine, 4 }; +static const rc_memory_regions_t rc_memory_regions_pc_engine_cd = { _rc_memory_regions_pc_engine_cd, 4 }; /* ===== PC-FX ===== */ /* http://daifukkat.su/pcfx/data/memmap.html */ @@ -711,6 +785,13 @@ static const rc_memory_region_t _rc_memory_regions_thomson_to8[] = { }; static const rc_memory_regions_t rc_memory_regions_thomson_to8 = { _rc_memory_regions_thomson_to8, 1 }; +/* ===== TI-83 ===== */ +/* https://tutorials.eeems.ca/ASMin28Days/lesson/day03.html#mem */ +static const rc_memory_region_t _rc_memory_regions_ti83[] = { + { 0x000000U, 0x007FFFU, 0x008000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, +}; +static const rc_memory_regions_t rc_memory_regions_ti83 = { _rc_memory_regions_ti83, 1 }; + /* ===== TIC-80 ===== */ /* https://github.com/nesbox/TIC-80/wiki/RAM */ static const rc_memory_region_t _rc_memory_regions_tic80[] = { @@ -727,6 +808,13 @@ static const rc_memory_region_t _rc_memory_regions_tic80[] = { }; static const rc_memory_regions_t rc_memory_regions_tic80 = { _rc_memory_regions_tic80, 10 }; +/* ===== Uzebox ===== */ +/* https://uzebox.org/index.php */ +static const rc_memory_region_t _rc_memory_regions_uzebox[] = { + { 0x000000U, 0x000FFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_uzebox = { _rc_memory_regions_uzebox, 1 }; + /* ===== Vectrex ===== */ /* https://roadsidethoughts.com/vectrex/vectrex-memory-map.htm */ static const rc_memory_region_t _rc_memory_regions_vectrex[] = { @@ -761,6 +849,14 @@ static const rc_memory_region_t _rc_memory_regions_wasm4[] = { }; static const rc_memory_regions_t rc_memory_regions_wasm4 = { _rc_memory_regions_wasm4, 1 }; +/* ===== Wii ===== */ +/* https://wiibrew.org/wiki/Memory_map */ +static const rc_memory_region_t _rc_memory_regions_wii[] = { + { 0x00000000U, 0x017FFFFF, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x01800000U, 0x057FFFFF, 0x90000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } +}; +static const rc_memory_regions_t rc_memory_regions_wii = { _rc_memory_regions_wii, 2 }; + /* ===== WonderSwan ===== */ /* http://daifukkat.su/docs/wsman/#ovr_memmap */ static const rc_memory_region_t _rc_memory_regions_wonderswan[] = { @@ -810,6 +906,7 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) return &rc_memory_regions_atari7800; case RC_CONSOLE_ATARI_JAGUAR: + case RC_CONSOLE_ATARI_JAGUAR_CD: return &rc_memory_regions_atari_jaguar; case RC_CONSOLE_ATARI_LYNX: @@ -840,6 +937,9 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) case RC_CONSOLE_GAMEBOY_ADVANCE: return &rc_memory_regions_gameboy_advance; + case RC_CONSOLE_GAMECUBE: + return &rc_memory_regions_gamecube; + case RC_CONSOLE_GAME_GEAR: return &rc_memory_regions_game_gear; @@ -856,17 +956,20 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) return &rc_memory_regions_master_system; case RC_CONSOLE_MEGA_DRIVE: - case RC_CONSOLE_SEGA_32X: - /* NOTE: 32x adds an extra 512KB of memory (256KB RAM + 256KB VRAM) to the - * Genesis, but we currently don't support it. */ return &rc_memory_regions_megadrive; + case RC_CONSOLE_SEGA_32X: + return &rc_memory_regions_megadrive_32x; + case RC_CONSOLE_MSX: return &rc_memory_regions_msx; case RC_CONSOLE_NEOGEO_POCKET: return &rc_memory_regions_neo_geo_pocket; + case RC_CONSOLE_NEO_GEO_CD: + return &rc_memory_regions_neo_geo_cd; + case RC_CONSOLE_NINTENDO: return &rc_memory_regions_nes; @@ -876,6 +979,9 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) case RC_CONSOLE_NINTENDO_DS: return &rc_memory_regions_nintendo_ds; + case RC_CONSOLE_NINTENDO_DSI: + return &rc_memory_regions_nintendo_dsi; + case RC_CONSOLE_ORIC: return &rc_memory_regions_oric; @@ -885,6 +991,9 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) case RC_CONSOLE_PC_ENGINE: return &rc_memory_regions_pc_engine; + case RC_CONSOLE_PC_ENGINE_CD: + return &rc_memory_regions_pc_engine_cd; + case RC_CONSOLE_PCFX: return &rc_memory_regions_pcfx; @@ -921,9 +1030,15 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) case RC_CONSOLE_THOMSONTO8: return &rc_memory_regions_thomson_to8; + case RC_CONSOLE_TI83: + return &rc_memory_regions_ti83; + case RC_CONSOLE_TIC80: return &rc_memory_regions_tic80; + case RC_CONSOLE_UZEBOX: + return &rc_memory_regions_uzebox; + case RC_CONSOLE_VECTREX: return &rc_memory_regions_vectrex; @@ -933,6 +1048,9 @@ const rc_memory_regions_t* rc_console_memory_regions(int console_id) case RC_CONSOLE_WASM4: return &rc_memory_regions_wasm4; + case RC_CONSOLE_WII: + return &rc_memory_regions_wii; + case RC_CONSOLE_WONDERSWAN: return &rc_memory_regions_wonderswan; diff --git a/dep/rcheevos/src/rcheevos/memref.c b/dep/rcheevos/src/rcheevos/memref.c index f4ea6ac28..fff2a6bca 100644 --- a/dep/rcheevos/src/rcheevos/memref.c +++ b/dep/rcheevos/src/rcheevos/memref.c @@ -95,6 +95,7 @@ int rc_parse_memref(const char** memaddr, char* size, unsigned* address) { switch (*aux++) { case 'f': case 'F': *size = RC_MEMSIZE_FLOAT; break; case 'm': case 'M': *size = RC_MEMSIZE_MBF32; break; + case 'l': case 'L': *size = RC_MEMSIZE_MBF32_LE; break; default: return RC_INVALID_FP_OPERAND; @@ -201,6 +202,21 @@ static void rc_transform_memref_mbf32(rc_typed_value_t* value) { value->type = RC_VALUE_TYPE_FLOAT; } +static void rc_transform_memref_mbf32_le(rc_typed_value_t* value) { + /* decodes a Microsoft Binary Format float */ + /* Locomotive BASIC (CPC) uses MBF40, but in little endian format */ + const unsigned mantissa = value->value.u32 & 0x007FFFFF; + const int exponent = (int)(value->value.u32 >> 24) - 129; + const int sign = (value->value.u32 & 0x00800000); + + if (mantissa == 0 && exponent == -129) + value->value.f32 = (sign) ? -0.0f : 0.0f; + else + value->value.f32 = rc_build_float(mantissa, exponent, sign); + + value->type = RC_VALUE_TYPE_FLOAT; +} + static const unsigned char rc_bits_set[16] = { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 }; void rc_transform_memref_value(rc_typed_value_t* value, char size) { @@ -293,6 +309,10 @@ void rc_transform_memref_value(rc_typed_value_t* value, char size) { rc_transform_memref_mbf32(value); break; + case RC_MEMSIZE_MBF32_LE: + rc_transform_memref_mbf32_le(value); + break; + default: break; } @@ -319,6 +339,7 @@ static const unsigned rc_memref_masks[] = { 0xffffffff, /* RC_MEMSIZE_32_BITS_BE */ 0xffffffff, /* RC_MEMSIZE_FLOAT */ 0xffffffff, /* RC_MEMSIZE_MBF32 */ + 0xffffffff, /* RC_MEMSIZE_MBF32_LE */ 0xffffffff /* RC_MEMSIZE_VARIABLE */ }; @@ -354,6 +375,7 @@ static const char rc_memref_shared_sizes[] = { RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_32_BITS_BE */ RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_FLOAT */ RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32 */ + RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32_LE */ RC_MEMSIZE_32_BITS /* RC_MEMSIZE_VARIABLE */ }; @@ -365,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; @@ -422,7 +444,7 @@ void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memref_t** memrefs) *memrefs = 0; } -static unsigned rc_get_memref_value_value(rc_memref_value_t* memref, int operand_type) { +static unsigned rc_get_memref_value_value(const rc_memref_value_t* memref, int operand_type) { switch (operand_type) { /* most common case explicitly first, even though it could be handled by default case. diff --git a/dep/rcheevos/src/rcheevos/operand.c b/dep/rcheevos/src/rcheevos/operand.c index 1501607ff..f8abbbd7b 100644 --- a/dep/rcheevos/src/rcheevos/operand.c +++ b/dep/rcheevos/src/rcheevos/operand.c @@ -59,6 +59,8 @@ static int rc_parse_operand_lua(rc_operand_t* self, const char** memaddr, rc_par self->value.luafunc = luaL_ref(parse->L, LUA_REGISTRYINDEX); } +#else + (void)parse; #endif /* RC_DISABLE_LUA */ self->type = RC_OPERAND_LUA; @@ -300,6 +302,7 @@ int rc_operand_is_float_memref(const rc_operand_t* self) { switch (self->size) { case RC_MEMSIZE_FLOAT: case RC_MEMSIZE_MBF32: + case RC_MEMSIZE_MBF32_LE: return 1; default: @@ -319,6 +322,13 @@ int rc_operand_is_memref(const rc_operand_t* self) { } } +int rc_operand_is_float(const rc_operand_t* self) { + if (self->type == RC_OPERAND_FP) + return 1; + + return rc_operand_is_float_memref(self); +} + unsigned rc_transform_operand_value(unsigned value, const rc_operand_t* self) { switch (self->type) { diff --git a/dep/rcheevos/src/rcheevos/rc_client.c b/dep/rcheevos/src/rcheevos/rc_client.c new file mode 100644 index 000000000..76b7bf127 --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_client.c @@ -0,0 +1,4669 @@ +#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 */ + +/* clock_t can wrap. For most cases, we won't have to worry about it because it won't + * overflow until after over a month of runtime. But some cases can overflow in as short as + * 36 minutes. Use substraction as a secondary check to ensure an overflow hasn't occurred. */ +#define RC_CLIENT_CLOCK_IS_BEFORE(clk, cmp_clk) (clk < cmp_clk && (cmp_clk - clk) > 0) + +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_begin_fetch_game_data(rc_client_load_state_t* callback_data); +static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game); +static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message); +static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, const char* hash, const char* file_path); +static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, clock_t now); +static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset); +static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game); +static void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard); +static void rc_client_reschedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* callback, clock_t when); + +/* ===== 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[2048]; + +#ifdef __STDC_WANT_SECURE_LIB__ + vsprintf_s(buffer, sizeof(buffer), format, args); +#elif __STDC_VERSION__ >= 199901L /* vsnprintf requires c99 */ + vsnprintf(buffer, sizeof(buffer), format, args); +#else /* c89 doesn't have a size-limited vsprintf function - assume the buffer is large enough */ + 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_should_retry(const rc_api_server_response_t* server_response) +{ + switch (server_response->http_status_code) { + case 502: /* 502 Bad Gateway */ + /* nginx connection pool full. retry */ + return 1; + + case 503: /* 503 Service Temporarily Unavailable */ + /* site is in maintenance mode. retry */ + return 1; + + case 429: /* 429 Too Many Requests */ + /* too many unlocks occurred at the same time */ + return 1; + + default: + return 0; + } +} + +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_server_response(&login_response, server_response); + 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; + } + + game->max_valid_address = max_address; + 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 = clock() + 30 * CLOCKS_PER_SEC; + 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_server_response(&start_session_response, server_response); + 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_server_response(&fetch_user_unlocks_response, server_response); + 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_client_achievement_info_t* scan; + 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->created_time = read->created; + achievement->updated_time = read->updated; + + scan = achievement; + while (scan > achievements) { + --scan; + if (strcmp(scan->author, read->author) == 0) { + achievement->author = scan->author; + break; + } + } + if (!achievement->author) + achievement->author = rc_buf_strcpy(buffer, read->author); + + ++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; + leaderboard->hidden = (uint8_t)read->hidden; + + 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_server_response(&fetch_game_data_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_game_data_response.response); + + 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; + + if (load_state->game->public_.console_id != RC_CONSOLE_UNKNOWN && + fetch_game_data_response.console_id != load_state->game->public_.console_id) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Data for game %u is for console %u, expecting console %u", + fetch_game_data_response.id, fetch_game_data_response.console_id, load_state->game->public_.console_id); + } + + /* 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->game_hash && + load_state->game->media_hash->game_hash->next) { + /* multiple hashes were tried, create a CSV */ + struct rc_client_game_hash_t* game_hash = load_state->game->media_hash->game_hash; + int count = 1; + char* ptr; + size_t size; + + size = strlen(game_hash->hash) + 1; + while (game_hash->next) { + game_hash = game_hash->next; + size += strlen(game_hash->hash) + 1; + count++; + } + + ptr = (char*)rc_buf_alloc(&load_state->game->buffer, size); + ptr += size - 1; + *ptr = '\0'; + game_hash = load_state->game->media_hash->game_hash; + do { + const size_t hash_len = strlen(game_hash->hash); + ptr -= hash_len; + memcpy(ptr, game_hash->hash, hash_len); + + game_hash = game_hash->next; + if (!game_hash) + break; + + ptr--; + *ptr = ','; + } while (1); + + load_state->game->public_.hash = ptr; + load_state->game->public_.console_id = RC_CONSOLE_UNKNOWN; + } 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_server_response(&resolve_hash_response, server_response); + 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); +} + +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_hash_iterator_t* rc_client_get_load_state_hash_iterator(rc_client_t* client) +{ + if (client && client->state.load) + return &client->state.load->hash_iterator; + + return NULL; +} + +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; + + if (console_id == RC_CONSOLE_UNKNOWN) { + rc_hash_initialize_iterator(&load_state->hash_iterator, file_path, data, data_size); + + 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 { + /* ASSERT: hash_iterator->index and hash_iterator->consoles[0] will be 0 from calloc */ + load_state->hash_console_id = console_id; + + if (data != NULL) { + if (!rc_hash_generate_from_buffer(hash, console_id, data, data_size)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + } + else { + if (!rc_hash_generate_from_file(hash, console_id, file_path)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; + } + } + } + + return rc_client_load_game(load_state, hash, file_path); +} + +static void rc_client_game_mark_ui_to_be_hidden(rc_client_t* client, rc_client_game_info_t* game) +{ + 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; + rc_client_subset_info_t* subset; + + for (subset = game->subsets; subset; subset = subset->next) { + achievement = subset->achievements; + achievement_stop = achievement + subset->public_.num_achievements; + for (; achievement < achievement_stop; ++achievement) { + if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE && + achievement->trigger && achievement->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; + } + } + + leaderboard = subset->leaderboards; + leaderboard_stop = leaderboard + subset->public_.num_leaderboards; + for (; leaderboard < leaderboard_stop; ++leaderboard) { + if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) + rc_client_release_leaderboard_tracker(game, leaderboard); + } + } + + rc_client_hide_progress_tracker(client, game); +} + +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; + + if (game != NULL) + rc_client_game_mark_ui_to_be_hidden(client, game); + + 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_raise_pending_events(client, game); + + 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_server_response(&resolve_hash_response, server_response); + 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[rc_client_map_bucket(achievement->public_.bucket, grouping)]++; + } + } + } + + 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 (rc_client_map_bucket(achievement->public_.bucket, grouping) == 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_.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 = 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, clock_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_server_response(&award_achievement_response, server_response); + 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 && !rc_client_should_retry(server_response)) { + /* 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 = clock() + delay * CLOCKS_PER_SEC; + + 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) { + if (leaderboard->hidden) + continue; + + 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->hidden) + *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->hidden) + *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 && tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE) + 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, clock_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_server_response(&submit_lboard_entry_response, server_response); + 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 && !rc_client_should_retry(server_response)) { + /* 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 = clock() + delay * CLOCKS_PER_SEC; + + 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_server_response(&lbinfo_response, server_response); + 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_server_response(&response, server_response); + 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, clock_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 * CLOCKS_PER_SEC; + 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; + + /* if processing_memref is not set, this occurred following a pointer chain. ignore it. */ + if (!client->state.processing_memref) + return; + + /* 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 we know the address is out of range, and it's part of a pointer chain + * (processing_memref is null), don't bother processing it. */ + if (address > client->game->max_valid_address && !client->state.processing_memref) + return 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; + + /* if we know the address is out of range, and it's part of a pointer chain + * (processing_memref is null), don't bother processing it. */ + if (address > client->game->max_valid_address && !client->state.processing_memref) + return 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; + + 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 > client->game->progress_tracker.progress) { + client->game->progress_tracker.progress = progress; + client->game->progress_tracker.achievement = achievement; + client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER; + subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT; + achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_UPDATE; + } + } + + /* 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_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) +{ + /* ASSERT: this should only be called if the mutex is held */ + + if (game->progress_tracker.hide_callback && + game->progress_tracker.hide_callback->when && + game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) { + rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, 0); + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE; + game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER; + } +} + +static void rc_client_progress_tracker_timer_elapsed(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, clock_t now) +{ + rc_client_event_t client_event; + memset(&client_event, 0, sizeof(client_event)); + + rc_mutex_lock(&client->state.mutex); + if (client->game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) { + client->game->progress_tracker.hide_callback->when = 0; + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE; + } + rc_mutex_unlock(&client->state.mutex); + + if (client_event.type) + client->callbacks.event_handler(&client_event, client); +} + +static void rc_client_do_frame_update_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) +{ + if (!game->progress_tracker.hide_callback) { + game->progress_tracker.hide_callback = (rc_client_scheduled_callback_data_t*) + rc_buf_alloc(&game->buffer, sizeof(rc_client_scheduled_callback_data_t)); + memset(game->progress_tracker.hide_callback, 0, sizeof(rc_client_scheduled_callback_data_t)); + game->progress_tracker.hide_callback->callback = rc_client_progress_tracker_timer_elapsed; + } + + if (game->progress_tracker.hide_callback->when == 0) + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW; + else + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE; + + rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, clock() + 2 * CLOCKS_PER_SEC); +} + +static void rc_client_raise_progress_tracker_events(rc_client_t* client, rc_client_game_info_t* game) +{ + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + switch (game->progress_tracker.action) { + case RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW; + break; + case RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE; + break; + default: + client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE; + break; + } + game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE; + + client_event.achievement = &game->progress_tracker.achievement->public_; + client->callbacks.event_handler(&client_event, client); +} + +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_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; + + if (old_state != RC_LBOARD_STATE_STARTED) + rc_client_allocate_leaderboard_tracker(client->game, leaderboard); + else + rc_client_update_leaderboard_tracker(client->game, leaderboard); + + 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_game_info_t* game) +{ + rc_client_leaderboard_tracker_info_t* tracker = game->leaderboard_trackers; + rc_client_event_t client_event; + + memset(&client_event, 0, sizeof(client_event)); + + tracker = 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_; + + /* update display text for new trackers or updated trackers */ + if (tracker->pending_events & (RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW | RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE)) + 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_HIDE) { + if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) { + /* request to show and hide in the same frame - ignore the event */ + } + else { + client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE; + client->callbacks.event_handler(&client_event, client); + } + } + else 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_game_info_t* game) +{ + rc_client_subset_info_t* subset; + + /* raise tracker events before leaderboard events so formatted values are updated for leaderboard events */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER) + rc_client_raise_leaderboard_tracker_events(client, game); + + for (subset = game->subsets; subset; subset = subset->next) + rc_client_subset_raise_pending_events(client, subset); + + /* raise progress tracker events after achievement events so formatted values are updated for tracker event */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER) + rc_client_raise_progress_tracker_events(client, game); + + /* if any achievements were unlocked, resync the active achievements list */ + if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS) { + rc_mutex_lock(&client->state.mutex); + rc_client_update_active_achievements(game); + rc_mutex_unlock(&client->state.mutex); + } + + game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE; +} + +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); + + client->game->progress_tracker.progress = 0.0; + for (subset = client->game->subsets; subset; subset = subset->next) { + if (subset->active) + rc_client_do_frame_process_achievements(client, subset); + } + if (client->game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER) + rc_client_do_frame_update_progress_tracker(client, client->game); + + 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, client->game); + } + + 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 clock_t now = clock(); + + do { + rc_mutex_lock(&client->state.mutex); + scheduled_callback = client->state.scheduled_callbacks; + if (scheduled_callback) { + if (RC_CLIENT_CLOCK_IS_BEFORE(now, scheduled_callback->when)) { + /* 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 || RC_CLIENT_CLOCK_IS_BEFORE(scheduled_callback->when, next->when)) { + scheduled_callback->next = next; + *last = scheduled_callback; + break; + } + + last = &next->next; + } while (1); + + rc_mutex_unlock(&client->state.mutex); +} + +static void rc_client_reschedule_callback(rc_client_t* client, + rc_client_scheduled_callback_data_t* callback, clock_t when) +{ + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + + /* ASSERT: this should only be called if the mutex is held */ + + callback->when = when; + + last = &client->state.scheduled_callbacks; + do { + next = *last; + + if (next == callback) { + if (when == 0) { + /* request to unschedule the callback */ + *last = next->next; + next->next = NULL; + break; + } + + if (!next->next) { + /* end of list, just append it */ + break; + } + + if (RC_CLIENT_CLOCK_IS_BEFORE(when, next->next->when)) { + /* already in the correct place */ + break; + } + + /* remove from current position - will insert later */ + *last = next->next; + next->next = NULL; + continue; + } + + if (!next || RC_CLIENT_CLOCK_IS_BEFORE(when, next->when)) { + /* insert here */ + callback->next = next; + *last = callback; + break; + } + + last = &next->next; + } while (1); +} + +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_hide_progress_tracker(client, client->game); + rc_client_reset_all(client); + + rc_mutex_unlock(&client->state.mutex); + + rc_client_raise_pending_events(client, client->game); +} + +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_LEADERBOARD; + } + } +} + +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); + + rc_client_hide_progress_tracker(client, client->game); + + 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, client->game); + + 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, client->game); + } + } +} + +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/dep/rcheevos/src/rcheevos/rc_client_internal.h b/dep/rcheevos/src/rcheevos/rc_client_internal.h new file mode 100644 index 000000000..9d0dd9f99 --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_client_internal.h @@ -0,0 +1,290 @@ +#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, clock_t now); + +typedef struct rc_client_scheduled_callback_data_t +{ + clock_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_UPDATE = (1 << 4) /* not a real event, just triggers update */ +}; + +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; + + const char* author; + time_t created_time; + time_t updated_time; +} rc_client_achievement_info_t; + +enum { + RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE, + RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW, + RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE, + RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE +}; + +typedef struct rc_client_progress_tracker_t { + rc_client_achievement_info_t* achievement; + float progress; + + rc_client_scheduled_callback_data_t* hide_callback; + uint8_t action; +} rc_client_progress_tracker_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; + uint8_t hidden; +} 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), + RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER = (1 << 3) +}; + +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; + +rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash); + +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_progress_tracker_t progress_tracker; + + rc_client_subset_info_t* subsets; + + rc_client_media_hash_t* media_hash; + + rc_runtime_t runtime; + + uint32_t max_valid_address; + + 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 */ + +/* helper functions for unit tests */ +struct rc_hash_iterator; +struct rc_hash_iterator* rc_client_get_load_state_hash_iterator(rc_client_t* client); +/* end helper functions for unit tests */ + +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_CLIENT_INTERNAL_H */ diff --git a/dep/rcheevos/src/rcheevos/rc_compat.h b/dep/rcheevos/src/rcheevos/rc_compat.h index 62c362f47..e22f9b84e 100644 --- a/dep/rcheevos/src/rcheevos/rc_compat.h +++ b/dep/rcheevos/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 */ @@ -30,6 +32,9 @@ extern "C" { #elif __STDC_VERSION__ < 199901L /* 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, ...); @@ -53,6 +58,40 @@ extern "C" { #endif /* __STDC_VERSION__ < 199901L */ +#ifndef __STDC_WANT_SECURE_LIB__ + /* _CRT_SECURE_NO_WARNINGS redefinitions */ + #define strcpy_s(dest, sz, src) strcpy(dest, src) + #define sscanf_s sscanf + + /* NOTE: Microsoft secure gmtime_s parameter order differs from C11 standard */ + #include + extern struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer); + #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/dep/rcheevos/src/rcheevos/rc_internal.h b/dep/rcheevos/src/rcheevos/rc_internal.h index c98f71309..e58ca53de 100644 --- a/dep/rcheevos/src/rcheevos/rc_internal.h +++ b/dep/rcheevos/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; @@ -83,6 +86,8 @@ typedef struct { } rc_typed_value_t; +#define RC_MEASURED_UNKNOWN 0xFFFFFFFF + typedef struct { rc_typed_value_t add_value;/* AddSource/SubSource */ int add_hits; /* AddHits */ @@ -130,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); @@ -138,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); @@ -146,6 +154,22 @@ rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, in int rc_test_condset(rc_condset_t* self, rc_eval_state_t* eval_state); void rc_reset_condset(rc_condset_t* self); +enum { + RC_PROCESSING_COMPARE_DEFAULT = 0, + RC_PROCESSING_COMPARE_MEMREF_TO_CONST, + RC_PROCESSING_COMPARE_MEMREF_TO_DELTA, + RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF, + RC_PROCESSING_COMPARE_DELTA_TO_MEMREF, + RC_PROCESSING_COMPARE_DELTA_TO_CONST, + RC_PROCESSING_COMPARE_MEMREF_TO_CONST_TRANSFORMED, + RC_PROCESSING_COMPARE_MEMREF_TO_DELTA_TRANSFORMED, + RC_PROCESSING_COMPARE_MEMREF_TO_MEMREF_TRANSFORMED, + RC_PROCESSING_COMPARE_DELTA_TO_MEMREF_TRANSFORMED, + RC_PROCESSING_COMPARE_DELTA_TO_CONST_TRANSFORMED, + RC_PROCESSING_COMPARE_ALWAYS_TRUE, + RC_PROCESSING_COMPARE_ALWAYS_FALSE +}; + rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, int is_indirect); int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state); void rc_evaluate_condition_value(rc_typed_value_t* value, rc_condition_t* self, rc_eval_state_t* eval_state); @@ -154,10 +178,12 @@ int rc_condition_is_combining(const rc_condition_t* self); int rc_parse_operand(rc_operand_t* self, const char** memaddr, int is_indirect, rc_parse_state_t* parse); void rc_evaluate_operand(rc_typed_value_t* value, rc_operand_t* self, rc_eval_state_t* eval_state); int rc_operand_is_float_memref(const rc_operand_t* self); +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); @@ -165,6 +191,7 @@ void rc_typed_value_convert(rc_typed_value_t* value, char new_type); void rc_typed_value_add(rc_typed_value_t* value, const rc_typed_value_t* amount); void rc_typed_value_multiply(rc_typed_value_t* value, const rc_typed_value_t* amount); void rc_typed_value_divide(rc_typed_value_t* value, const rc_typed_value_t* amount); +void rc_typed_value_negate(rc_typed_value_t* value); int rc_typed_value_compare(const rc_typed_value_t* value1, const rc_typed_value_t* value2, char oper); void rc_typed_value_from_memref_value(rc_typed_value_t* value, const rc_memref_value_t* memref); diff --git a/dep/rcheevos/src/rcheevos/rc_libretro.c b/dep/rcheevos/src/rcheevos/rc_libretro.c new file mode 100644 index 000000000..eab44ef1b --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_libretro.c @@ -0,0 +1,816 @@ +/* This file provides a series of functions for integrating RetroAchievements with libretro. + * These functions will be called by a libretro frontend to validate certain expected behaviors + * and simplify mapping core data to the RAIntegration DLL. + * + * Originally designed to be shared between RALibretro and RetroArch, but will simplify + * integrating with any other frontends. + */ + +#include "rc_libretro.h" + +#include "rc_consoles.h" +#include "rc_compat.h" + +#include +#include + +/* internal helper functions in hash.c */ +extern void* rc_file_open(const char* path); +extern void rc_file_seek(void* file_handle, int64_t offset, int origin); +extern int64_t rc_file_tell(void* file_handle); +extern size_t rc_file_read(void* file_handle, void* buffer, int requested_bytes); +extern void rc_file_close(void* file_handle); +extern int rc_path_compare_extension(const char* path, const char* ext); +extern int rc_hash_error(const char* message); + + +static rc_libretro_message_callback rc_libretro_verbose_message_callback = NULL; + +/* a value that starts with a comma is a CSV. + * if it starts with an exclamation point, it's everything but the provided value. + * if it starts with an exclamntion point followed by a comma, it's everything but the CSV values. + * values are case-insensitive */ +typedef struct rc_disallowed_core_settings_t +{ + const char* library_name; + const rc_disallowed_setting_t* disallowed_settings; +} rc_disallowed_core_settings_t; + +static const rc_disallowed_setting_t _rc_disallowed_bsnes_settings[] = { + { "bsnes_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_cap32_settings[] = { + { "cap32_autorun", "disabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_dolphin_settings[] = { + { "dolphin_cheats_enabled", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_duckstation_settings[] = { + { "duckstation_CDROM.LoadImagePatches", "true" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_ecwolf_settings[] = { + { "ecwolf-invulnerability", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_fbneo_settings[] = { + { "fbneo-allow-patched-romsets", "enabled" }, + { "fbneo-cheat-*", "!,Disabled,0 - Disabled" }, + { "fbneo-dipswitch-*", "Universe BIOS*" }, + { "fbneo-neogeo-mode", "UNIBIOS" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_fceumm_settings[] = { + { "fceumm_region", ",PAL,Dendy" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_gpgx_settings[] = { + { "genesis_plus_gx_lock_on", ",action replay (pro),game genie" }, + { "genesis_plus_gx_region_detect", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_gpgx_wide_settings[] = { + { "genesis_plus_gx_wide_lock_on", ",action replay (pro),game genie" }, + { "genesis_plus_gx_wide_region_detect", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_mesen_settings[] = { + { "mesen_region", ",PAL,Dendy" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_mesen_s_settings[] = { + { "mesen-s_region", "PAL" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_pcsx_rearmed_settings[] = { + { "pcsx_rearmed_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_picodrive_settings[] = { + { "picodrive_region", ",Europe,Japan PAL" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_ppsspp_settings[] = { + { "ppsspp_cheats", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_quasi88_settings[] = { + { "q88_cpu_clock", ",1,2" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_smsplus_settings[] = { + { "smsplus_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_snes9x_settings[] = { + { "snes9x_gfx_clip", "disabled" }, + { "snes9x_gfx_transp", "disabled" }, + { "snes9x_layer_*", "disabled" }, + { "snes9x_region", "pal" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_vice_settings[] = { + { "vice_autostart", "disabled" }, /* autostart dictates initial load and reset from menu */ + { "vice_reset", "!autostart" }, /* reset dictates behavior when pressing reset button (END) */ + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_virtual_jaguar_settings[] = { + { "virtualjaguar_pal", "enabled" }, + { NULL, NULL } +}; + +static const rc_disallowed_core_settings_t rc_disallowed_core_settings[] = { + { "bsnes-mercury", _rc_disallowed_bsnes_settings }, + { "cap32", _rc_disallowed_cap32_settings }, + { "dolphin-emu", _rc_disallowed_dolphin_settings }, + { "DuckStation", _rc_disallowed_duckstation_settings }, + { "ecwolf", _rc_disallowed_ecwolf_settings }, + { "FCEUmm", _rc_disallowed_fceumm_settings }, + { "FinalBurn Neo", _rc_disallowed_fbneo_settings }, + { "Genesis Plus GX", _rc_disallowed_gpgx_settings }, + { "Genesis Plus GX Wide", _rc_disallowed_gpgx_wide_settings }, + { "Mesen", _rc_disallowed_mesen_settings }, + { "Mesen-S", _rc_disallowed_mesen_s_settings }, + { "PPSSPP", _rc_disallowed_ppsspp_settings }, + { "PCSX-ReARMed", _rc_disallowed_pcsx_rearmed_settings }, + { "PicoDrive", _rc_disallowed_picodrive_settings }, + { "QUASI88", _rc_disallowed_quasi88_settings }, + { "SMS Plus GX", _rc_disallowed_smsplus_settings }, + { "Snes9x", _rc_disallowed_snes9x_settings }, + { "VICE x64", _rc_disallowed_vice_settings }, + { "Virtual Jaguar", _rc_disallowed_virtual_jaguar_settings }, + { NULL, NULL } +}; + +static int rc_libretro_string_equal_nocase_wildcard(const char* test, const char* value) { + char c1, c2; + while ((c1 = *test++)) { + if (tolower(c1) != tolower(c2 = *value++)) + return (c2 == '*'); + } + + return (*value == '\0'); +} + +static int rc_libretro_match_value(const char* val, const char* match) { + /* if value starts with a comma, it's a CSV list of potential matches */ + if (*match == ',') { + do { + const char* ptr = ++match; + size_t size; + + while (*match && *match != ',') + ++match; + + size = match - ptr; + if (val[size] == '\0') { + if (memcmp(ptr, val, size) == 0) { + return 1; + } + else { + char buffer[128]; + memcpy(buffer, ptr, size); + buffer[size] = '\0'; + if (rc_libretro_string_equal_nocase_wildcard(buffer, val)) + return 1; + } + } + } while (*match == ','); + + return 0; + } + + /* a leading exclamation point means the provided value(s) are not forbidden (are allowed) */ + if (*match == '!') + return !rc_libretro_match_value(val, &match[1]); + + /* just a single value, attempt to match it */ + return rc_libretro_string_equal_nocase_wildcard(val, match); +} + +int rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value) { + const char* key; + size_t key_len; + + for (; disallowed_settings->setting; ++disallowed_settings) { + key = disallowed_settings->setting; + key_len = strlen(key); + + if (key[key_len - 1] == '*') { + if (memcmp(setting, key, key_len - 1) == 0) { + if (rc_libretro_match_value(value, disallowed_settings->value)) + return 0; + } + } + else { + if (memcmp(setting, key, key_len + 1) == 0) { + if (rc_libretro_match_value(value, disallowed_settings->value)) + return 0; + } + } + } + + return 1; +} + +const rc_disallowed_setting_t* rc_libretro_get_disallowed_settings(const char* library_name) { + const rc_disallowed_core_settings_t* core_filter = rc_disallowed_core_settings; + size_t library_name_length; + + if (!library_name || !library_name[0]) + return NULL; + + library_name_length = strlen(library_name) + 1; + while (core_filter->library_name) { + if (memcmp(core_filter->library_name, library_name, library_name_length) == 0) + return core_filter->disallowed_settings; + + ++core_filter; + } + + return NULL; +} + +typedef struct rc_disallowed_core_systems_t +{ + const char* library_name; + const int disallowed_consoles[4]; +} rc_disallowed_core_systems_t; + +static const rc_disallowed_core_systems_t rc_disallowed_core_systems[] = { + /* https://github.com/libretro/Mesen-S/issues/8 */ + { "Mesen-S", { RC_CONSOLE_GAMEBOY, RC_CONSOLE_GAMEBOY_COLOR, 0 }}, + { NULL, { 0 } } +}; + +int rc_libretro_is_system_allowed(const char* library_name, int console_id) { + const rc_disallowed_core_systems_t* core_filter = rc_disallowed_core_systems; + size_t library_name_length; + size_t i; + + if (!library_name || !library_name[0]) + return 1; + + library_name_length = strlen(library_name) + 1; + while (core_filter->library_name) { + if (memcmp(core_filter->library_name, library_name, library_name_length) == 0) { + for (i = 0; i < sizeof(core_filter->disallowed_consoles) / sizeof(core_filter->disallowed_consoles[0]); ++i) { + if (core_filter->disallowed_consoles[i] == console_id) + return 0; + } + break; + } + + ++core_filter; + } + + return 1; +} + +unsigned char* rc_libretro_memory_find_avail(const rc_libretro_memory_regions_t* regions, unsigned address, unsigned* avail) { + unsigned i; + + for (i = 0; i < regions->count; ++i) { + const size_t size = regions->size[i]; + if (address < size) { + if (regions->data[i] == NULL) + break; + + if (avail) + *avail = (unsigned)(size - address); + + return ®ions->data[i][address]; + } + + address -= (unsigned)size; + } + + if (avail) + *avail = 0; + + return NULL; +} + +unsigned char* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, unsigned address) { + 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; +} + +static void rc_libretro_verbose(const char* message) { + if (rc_libretro_verbose_message_callback) + rc_libretro_verbose_message_callback(message); +} + +static const char* rc_memory_type_str(int type) { + switch (type) + { + case RC_MEMORY_TYPE_SAVE_RAM: + return "SRAM"; + case RC_MEMORY_TYPE_VIDEO_RAM: + return "VRAM"; + case RC_MEMORY_TYPE_UNUSED: + return "UNUSED"; + default: + break; + } + + return "SYSTEM RAM"; +} + +static void rc_libretro_memory_register_region(rc_libretro_memory_regions_t* regions, int type, + unsigned char* data, size_t size, const char* description) { + if (size == 0) + return; + + if (regions->count == (sizeof(regions->size) / sizeof(regions->size[0]))) { + rc_libretro_verbose("Too many memory memory regions to register"); + return; + } + + if (!data && regions->count > 0 && !regions->data[regions->count - 1]) { + /* extend null region */ + regions->size[regions->count - 1] += size; + } + else if (data && regions->count > 0 && + data == (regions->data[regions->count - 1] + regions->size[regions->count - 1])) { + /* extend non-null region */ + regions->size[regions->count - 1] += size; + } + else { + /* create new region */ + regions->data[regions->count] = data; + regions->size[regions->count] = size; + ++regions->count; + } + + regions->total_size += size; + + if (rc_libretro_verbose_message_callback) { + char message[128]; + snprintf(message, sizeof(message), "Registered 0x%04X bytes of %s at $%06X (%s)", (unsigned)size, + rc_memory_type_str(type), (unsigned)(regions->total_size - size), description); + rc_libretro_verbose_message_callback(message); + } +} + +static void rc_libretro_memory_init_without_regions(rc_libretro_memory_regions_t* regions, + rc_libretro_get_core_memory_info_func get_core_memory_info) { + /* no regions specified, assume system RAM followed by save RAM */ + char description[64]; + rc_libretro_core_memory_info_t info; + + snprintf(description, sizeof(description), "offset 0x%06x", 0); + + get_core_memory_info(RETRO_MEMORY_SYSTEM_RAM, &info); + if (info.size) + rc_libretro_memory_register_region(regions, RC_MEMORY_TYPE_SYSTEM_RAM, info.data, info.size, description); + + get_core_memory_info(RETRO_MEMORY_SAVE_RAM, &info); + if (info.size) + rc_libretro_memory_register_region(regions, RC_MEMORY_TYPE_SAVE_RAM, info.data, info.size, description); +} + +static const struct retro_memory_descriptor* rc_libretro_memory_get_descriptor(const struct retro_memory_map* mmap, unsigned real_address, size_t* offset) +{ + const struct retro_memory_descriptor* desc = mmap->descriptors; + const struct retro_memory_descriptor* end = desc + mmap->num_descriptors; + + for (; desc < end; desc++) { + if (desc->select == 0) { + /* if select is 0, attempt to explcitly match the address */ + if (real_address >= desc->start && real_address < desc->start + desc->len) { + *offset = real_address - desc->start; + return desc; + } + } + else { + /* otherwise, attempt to match the address by matching the select bits */ + /* address is in the block if (addr & select) == (start & select) */ + if (((desc->start ^ real_address) & desc->select) == 0) { + /* get the relative offset of the address from the start of the memory block */ + unsigned reduced_address = real_address - (unsigned)desc->start; + + /* remove any bits from the reduced_address that correspond to the bits in the disconnect + * mask and collapse the remaining bits. this code was copied from the mmap_reduce function + * in RetroArch. i'm not exactly sure how it works, but it does. */ + unsigned disconnect_mask = (unsigned)desc->disconnect; + while (disconnect_mask) { + const unsigned tmp = (disconnect_mask - 1) & ~disconnect_mask; + reduced_address = (reduced_address & tmp) | ((reduced_address >> 1) & ~tmp); + disconnect_mask = (disconnect_mask & (disconnect_mask - 1)) >> 1; + } + + /* calculate the offset within the descriptor */ + *offset = reduced_address; + + /* sanity check - make sure the descriptor is large enough to hold the target address */ + if (reduced_address < desc->len) + return desc; + } + } + } + + *offset = 0; + return NULL; +} + +static void rc_libretro_memory_init_from_memory_map(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, + const rc_memory_regions_t* console_regions) { + char description[64]; + unsigned i; + unsigned char* region_start; + unsigned char* desc_start; + size_t desc_size; + size_t offset; + + for (i = 0; i < console_regions->num_regions; ++i) { + const rc_memory_region_t* console_region = &console_regions->region[i]; + size_t console_region_size = console_region->end_address - console_region->start_address + 1; + unsigned real_address = console_region->real_address; + unsigned disconnect_size = 0; + + while (console_region_size > 0) { + const struct retro_memory_descriptor* desc = rc_libretro_memory_get_descriptor(mmap, real_address, &offset); + if (!desc) { + if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { + snprintf(description, sizeof(description), "Could not map region starting at $%06X", + real_address - console_region->real_address + console_region->start_address); + rc_libretro_verbose(description); + } + + if (disconnect_size && console_region_size > disconnect_size) { + rc_libretro_memory_register_region(regions, console_region->type, NULL, disconnect_size, "null filler"); + console_region_size -= disconnect_size; + real_address += disconnect_size; + disconnect_size = 0; + continue; + } + + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size, "null filler"); + break; + } + + snprintf(description, sizeof(description), "descriptor %u, offset 0x%06X%s", + (unsigned)(desc - mmap->descriptors) + 1, (int)offset, desc->ptr ? "" : " [no pointer]"); + + if (desc->ptr) { + desc_start = (uint8_t*)desc->ptr + desc->offset; + region_start = desc_start + offset; + } + else { + region_start = NULL; + } + + desc_size = desc->len - offset; + if (desc->disconnect && desc_size > desc->disconnect) { + /* if we need to extract a disconnect bit, the largest block we can read is up to + * the next time that bit flips */ + /* https://stackoverflow.com/questions/12247186/find-the-lowest-set-bit */ + disconnect_size = (desc->disconnect & -((int)desc->disconnect)); + desc_size = disconnect_size - (real_address & (disconnect_size - 1)); + } + + if (console_region_size > desc_size) { + if (desc_size == 0) { + if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { + snprintf(description, sizeof(description), "Could not map region starting at $%06X", + real_address - console_region->real_address + console_region->start_address); + rc_libretro_verbose(description); + } + + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size, "null filler"); + console_region_size = 0; + } + else { + rc_libretro_memory_register_region(regions, console_region->type, region_start, desc_size, description); + console_region_size -= desc_size; + real_address += (unsigned)desc_size; + } + } + else { + rc_libretro_memory_register_region(regions, console_region->type, region_start, console_region_size, description); + console_region_size = 0; + } + } + } +} + +static unsigned rc_libretro_memory_console_region_to_ram_type(int region_type) { + switch (region_type) + { + case RC_MEMORY_TYPE_SAVE_RAM: + return RETRO_MEMORY_SAVE_RAM; + case RC_MEMORY_TYPE_VIDEO_RAM: + return RETRO_MEMORY_VIDEO_RAM; + default: + break; + } + + return RETRO_MEMORY_SYSTEM_RAM; +} + +static void rc_libretro_memory_init_from_unmapped_memory(rc_libretro_memory_regions_t* regions, + rc_libretro_get_core_memory_info_func get_core_memory_info, const rc_memory_regions_t* console_regions) { + char description[64]; + unsigned i, j; + rc_libretro_core_memory_info_t info; + size_t offset; + + for (i = 0; i < console_regions->num_regions; ++i) { + const rc_memory_region_t* console_region = &console_regions->region[i]; + const size_t console_region_size = console_region->end_address - console_region->start_address + 1; + const unsigned type = rc_libretro_memory_console_region_to_ram_type(console_region->type); + unsigned base_address = 0; + + for (j = 0; j <= i; ++j) { + const rc_memory_region_t* console_region2 = &console_regions->region[j]; + if (rc_libretro_memory_console_region_to_ram_type(console_region2->type) == type) { + base_address = console_region2->start_address; + break; + } + } + offset = console_region->start_address - base_address; + + get_core_memory_info(type, &info); + + if (offset < info.size) { + info.size -= offset; + + if (info.data) { + snprintf(description, sizeof(description), "offset 0x%06X", (int)offset); + info.data += offset; + } + else { + snprintf(description, sizeof(description), "null filler"); + } + } + else { + if (rc_libretro_verbose_message_callback && console_region->type != RC_MEMORY_TYPE_UNUSED) { + snprintf(description, sizeof(description), "Could not map region starting at $%06X", console_region->start_address); + rc_libretro_verbose(description); + } + + info.data = NULL; + info.size = 0; + } + + if (console_region_size > info.size) { + /* want more than what is available, take what we can and null fill the rest */ + rc_libretro_memory_register_region(regions, console_region->type, info.data, info.size, description); + rc_libretro_memory_register_region(regions, console_region->type, NULL, console_region_size - info.size, "null filler"); + } + else { + /* only take as much as we need */ + rc_libretro_memory_register_region(regions, console_region->type, info.data, console_region_size, description); + } + } +} + +int rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, + rc_libretro_get_core_memory_info_func get_core_memory_info, int console_id) { + const rc_memory_regions_t* console_regions = rc_console_memory_regions(console_id); + rc_libretro_memory_regions_t new_regions; + int has_valid_region = 0; + unsigned i; + + if (!regions) + return 0; + + memset(&new_regions, 0, sizeof(new_regions)); + + if (console_regions == NULL || console_regions->num_regions == 0) + rc_libretro_memory_init_without_regions(&new_regions, get_core_memory_info); + else if (mmap && mmap->num_descriptors != 0) + rc_libretro_memory_init_from_memory_map(&new_regions, mmap, console_regions); + else + rc_libretro_memory_init_from_unmapped_memory(&new_regions, get_core_memory_info, console_regions); + + /* determine if any valid regions were found */ + for (i = 0; i < new_regions.count; i++) { + if (new_regions.data[i]) { + has_valid_region = 1; + break; + } + } + + memcpy(regions, &new_regions, sizeof(*regions)); + return has_valid_region; +} + +void rc_libretro_memory_destroy(rc_libretro_memory_regions_t* regions) { + memset(regions, 0, sizeof(*regions)); +} + +void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, + const char* m3u_path, rc_libretro_get_image_path_func get_image_path) { + char image_path[1024]; + char* m3u_contents; + char* ptr; + int64_t file_len; + void* file_handle; + int index = 0; + + memset(hash_set, 0, sizeof(*hash_set)); + + if (!rc_path_compare_extension(m3u_path, "m3u")) + return; + + file_handle = rc_file_open(m3u_path); + if (!file_handle) + { + rc_hash_error("Could not open playlist"); + return; + } + + rc_file_seek(file_handle, 0, SEEK_END); + file_len = rc_file_tell(file_handle); + rc_file_seek(file_handle, 0, SEEK_SET); + + m3u_contents = (char*)malloc((size_t)file_len + 1); + rc_file_read(file_handle, m3u_contents, (int)file_len); + m3u_contents[file_len] = '\0'; + + rc_file_close(file_handle); + + ptr = m3u_contents; + do + { + /* ignore whitespace */ + while (isspace((int)*ptr)) + ++ptr; + + if (*ptr == '#') + { + /* ignore comment unless it's the special SAVEDISK extension */ + if (memcmp(ptr, "#SAVEDISK:", 10) == 0) + { + /* get the path to the save disk from the frontend, assign it a bogus hash so + * it doesn't get hashed later */ + if (get_image_path(index, image_path, sizeof(image_path))) + { + const char save_disk_hash[33] = "[SAVE DISK]"; + rc_libretro_hash_set_add(hash_set, image_path, -1, save_disk_hash); + ++index; + } + } + } + else + { + /* non-empty line, tally a file */ + ++index; + } + + /* find the end of the line */ + while (*ptr && *ptr != '\n') + ++ptr; + + } while (*ptr); + + free(m3u_contents); + + if (hash_set->entries_count > 0) + { + /* at least one save disk was found. make sure the core supports the #SAVEDISK: extension by + * asking for the last expected disk. if it's not found, assume no #SAVEDISK: support */ + if (!get_image_path(index - 1, image_path, sizeof(image_path))) + hash_set->entries_count = 0; + } +} + +void rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set) { + if (hash_set->entries) + free(hash_set->entries); + memset(hash_set, 0, sizeof(*hash_set)); +} + +static unsigned rc_libretro_djb2(const char* input) +{ + unsigned result = 5381; + char c; + + while ((c = *input++) != '\0') + result = ((result << 5) + result) + c; /* result = result * 33 + c */ + + return result; +} + +void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, + const char* path, int game_id, const char hash[33]) { + const unsigned path_djb2 = (path != NULL) ? rc_libretro_djb2(path) : 0; + struct rc_libretro_hash_entry_t* entry = NULL; + struct rc_libretro_hash_entry_t* scan; + struct rc_libretro_hash_entry_t* stop = hash_set->entries + hash_set->entries_count;; + + if (path_djb2) + { + /* attempt to match the path */ + for (scan = hash_set->entries; scan < stop; ++scan) + { + if (scan->path_djb2 == path_djb2) + { + entry = scan; + break; + } + } + } + + if (!entry) + { + /* entry not found, allocate a new one */ + if (hash_set->entries_size == 0) + { + hash_set->entries_size = 4; + hash_set->entries = (struct rc_libretro_hash_entry_t*) + malloc(hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t)); + } + else if (hash_set->entries_count == hash_set->entries_size) + { + hash_set->entries_size += 4; + hash_set->entries = (struct rc_libretro_hash_entry_t*)realloc(hash_set->entries, + hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t)); + } + + entry = hash_set->entries + hash_set->entries_count++; + } + + /* update the entry */ + entry->path_djb2 = path_djb2; + entry->game_id = game_id; + memcpy(entry->hash, hash, sizeof(entry->hash)); +} + +const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path) +{ + const unsigned path_djb2 = rc_libretro_djb2(path); + struct rc_libretro_hash_entry_t* scan = hash_set->entries; + struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count; + for (; scan < stop; ++scan) + { + if (scan->path_djb2 == path_djb2) + return scan->hash; + } + + return NULL; +} + +int rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash) +{ + struct rc_libretro_hash_entry_t* scan = hash_set->entries; + struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count; + for (; scan < stop; ++scan) + { + if (memcmp(scan->hash, hash, sizeof(scan->hash)) == 0) + return scan->game_id; + } + + return 0; +} diff --git a/dep/rcheevos/src/rcheevos/rc_libretro.h b/dep/rcheevos/src/rcheevos/rc_libretro.h new file mode 100644 index 000000000..089631bae --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_libretro.h @@ -0,0 +1,92 @@ +#ifndef RC_LIBRETRO_H +#define RC_LIBRETRO_H + +/* this file comes from the libretro repository, which is not an explicit submodule. + * the integration must set up paths appropriately to find it. */ +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/*****************************************************************************\ +| Disallowed Settings | +\*****************************************************************************/ + +typedef struct rc_disallowed_setting_t +{ + const char* setting; + const char* value; +} rc_disallowed_setting_t; + +const rc_disallowed_setting_t* rc_libretro_get_disallowed_settings(const char* library_name); +int rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value); +int rc_libretro_is_system_allowed(const char* library_name, int console_id); + +/*****************************************************************************\ +| Memory Mapping | +\*****************************************************************************/ + +/* specifies a function to call for verbose logging */ +typedef void (*rc_libretro_message_callback)(const char*); +void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback); + +#define RC_LIBRETRO_MAX_MEMORY_REGIONS 32 +typedef struct rc_libretro_memory_regions_t +{ + unsigned char* data[RC_LIBRETRO_MAX_MEMORY_REGIONS]; + size_t size[RC_LIBRETRO_MAX_MEMORY_REGIONS]; + size_t total_size; + unsigned count; +} rc_libretro_memory_regions_t; + +typedef struct rc_libretro_core_memory_info_t +{ + unsigned char* data; + size_t size; +} rc_libretro_core_memory_info_t; + +typedef void (*rc_libretro_get_core_memory_info_func)(unsigned id, rc_libretro_core_memory_info_t* info); + +int rc_libretro_memory_init(rc_libretro_memory_regions_t* regions, const struct retro_memory_map* mmap, + rc_libretro_get_core_memory_info_func get_core_memory_info, int console_id); +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 | +\*****************************************************************************/ + +typedef struct rc_libretro_hash_entry_t +{ + uint32_t path_djb2; + int game_id; + char hash[33]; +} rc_libretro_hash_entry_t; + +typedef struct rc_libretro_hash_set_t +{ + struct rc_libretro_hash_entry_t* entries; + uint16_t entries_count; + uint16_t entries_size; +} rc_libretro_hash_set_t; + +typedef int (*rc_libretro_get_image_path_func)(unsigned index, char* buffer, size_t buffer_size); + +void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, + const char* m3u_path, rc_libretro_get_image_path_func get_image_path); +void rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set); + +void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, + const char* path, int game_id, const char hash[33]); +const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* hash_set, const char* path); +int rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_set, const char* hash); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_LIBRETRO_H */ diff --git a/dep/rcheevos/src/rcheevos/rc_validate.c b/dep/rcheevos/src/rcheevos/rc_validate.c new file mode 100644 index 000000000..1751b890e --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_validate.c @@ -0,0 +1,847 @@ +#include "rc_validate.h" + +#include "rc_compat.h" +#include "rc_consoles.h" +#include "rc_internal.h" + +#include +#include + +static int rc_validate_memref(const rc_memref_t* memref, char result[], const size_t result_size, int console_id, unsigned max_address) +{ + if (memref->address > max_address) { + snprintf(result, result_size, "Address %04X out of range (max %04X)", memref->address, max_address); + return 0; + } + + switch (console_id) { + case RC_CONSOLE_NINTENDO: + if (memref->address >= 0x0800 && memref->address <= 0x1FFF) { + snprintf(result, result_size, "Mirror RAM may not be exposed by emulator (address %04X)", memref->address); + return 0; + } + break; + + case RC_CONSOLE_GAMEBOY: + case RC_CONSOLE_GAMEBOY_COLOR: + if (memref->address >= 0xE000 && memref->address <= 0xFDFF) { + snprintf(result, result_size, "Echo RAM may not be exposed by emulator (address %04X)", memref->address); + return 0; + } + break; + + case RC_CONSOLE_PLAYSTATION: + if (memref->address <= 0xFFFF) { + snprintf(result, result_size, "Kernel RAM may not be initialized without real BIOS (address %04X)", memref->address); + return 0; + } + break; + } + + return 1; +} + +int rc_validate_memrefs(const rc_memref_t* memref, char result[], const size_t result_size, unsigned max_address) +{ + while (memref) { + if (!rc_validate_memref(memref, result, result_size, 0, max_address)) + return 0; + + memref = memref->next; + } + + return 1; +} + +static unsigned rc_console_max_address(int console_id) +{ + const rc_memory_regions_t* memory_regions; + memory_regions = rc_console_memory_regions(console_id); + if (memory_regions && memory_regions->num_regions > 0) + return memory_regions->region[memory_regions->num_regions - 1].end_address; + + return 0xFFFFFFFF; +} + +int rc_validate_memrefs_for_console(const rc_memref_t* memref, char result[], const size_t result_size, int console_id) +{ + const unsigned max_address = rc_console_max_address(console_id); + while (memref) { + if (!rc_validate_memref(memref, result, result_size, console_id, max_address)) + return 0; + + memref = memref->next; + } + + return 1; +} + +static unsigned rc_max_value(const rc_operand_t* operand) +{ + if (operand->type == RC_OPERAND_CONST) + return operand->value.num; + + if (!rc_operand_is_memref(operand)) + return 0xFFFFFFFF; + + switch (operand->size) { + case RC_MEMSIZE_BIT_0: + case RC_MEMSIZE_BIT_1: + case RC_MEMSIZE_BIT_2: + case RC_MEMSIZE_BIT_3: + case RC_MEMSIZE_BIT_4: + case RC_MEMSIZE_BIT_5: + case RC_MEMSIZE_BIT_6: + case RC_MEMSIZE_BIT_7: + return 1; + + case RC_MEMSIZE_LOW: + case RC_MEMSIZE_HIGH: + return 0xF; + + case RC_MEMSIZE_BITCOUNT: + return 8; + + case RC_MEMSIZE_8_BITS: + return (operand->type == RC_OPERAND_BCD) ? 165 : 0xFF; + + case RC_MEMSIZE_16_BITS: + case RC_MEMSIZE_16_BITS_BE: + return (operand->type == RC_OPERAND_BCD) ? 16665 : 0xFFFF; + + case RC_MEMSIZE_24_BITS: + case RC_MEMSIZE_24_BITS_BE: + return (operand->type == RC_OPERAND_BCD) ? 1666665 : 0xFFFFFF; + + default: + return (operand->type == RC_OPERAND_BCD) ? 166666665 : 0xFFFFFFFF; + } +} + +static unsigned rc_scale_value(unsigned value, char oper, const rc_operand_t* operand) +{ + switch (oper) { + case RC_OPERATOR_MULT: + { + unsigned long long scaled = ((unsigned long long)value) * rc_max_value(operand); + if (scaled > 0xFFFFFFFF) + return 0xFFFFFFFF; + + return (unsigned)scaled; + } + + case RC_OPERATOR_DIV: + { + const unsigned min_val = (operand->type == RC_OPERAND_CONST) ? operand->value.num : 1; + return value / min_val; + } + + case RC_OPERATOR_AND: + return rc_max_value(operand); + + case RC_OPERATOR_XOR: + return value | rc_max_value(operand); + + default: + return value; + } +} + +static int rc_validate_get_condition_index(const rc_condset_t* condset, const rc_condition_t* condition) +{ + int index = 1; + const rc_condition_t* scan; + for (scan = condset->conditions; scan != NULL; scan = scan->next) + { + if (scan == condition) + return index; + + ++index; + } + + return 0; +} + +static int rc_validate_range(unsigned min_val, unsigned max_val, char oper, unsigned max, char result[], const size_t result_size) +{ + switch (oper) { + case RC_OPERATOR_AND: + if (min_val > max) { + snprintf(result, result_size, "Mask has more bits than source"); + return 0; + } + else if (min_val == 0 && max_val == 0) { + snprintf(result, result_size, "Result of mask always 0"); + return 0; + } + break; + + case RC_OPERATOR_EQ: + if (min_val > max) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + break; + + case RC_OPERATOR_NE: + if (min_val > max) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + break; + + case RC_OPERATOR_GE: + if (min_val > max) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + if (max_val == 0) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + break; + + case RC_OPERATOR_GT: + if (min_val >= max) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + break; + + case RC_OPERATOR_LE: + if (min_val >= max) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + break; + + case RC_OPERATOR_LT: + if (min_val > max) { + snprintf(result, result_size, "Comparison is always true"); + return 0; + } + if (max_val == 0) { + snprintf(result, result_size, "Comparison is never true"); + return 0; + } + break; + } + + return 1; +} + +int rc_validate_condset_internal(const rc_condset_t* condset, char result[], const size_t result_size, int console_id, unsigned max_address) +{ + const rc_condition_t* cond; + char buffer[128]; + unsigned max_val; + int index = 1; + unsigned long long add_source_max = 0; + int in_add_hits = 0; + int in_add_address = 0; + int is_combining = 0; + + if (!condset) { + *result = '\0'; + return 1; + } + + for (cond = condset->conditions; cond; cond = cond->next, ++index) { + unsigned max = rc_max_value(&cond->operand1); + const int is_memref1 = rc_operand_is_memref(&cond->operand1); + const int is_memref2 = rc_operand_is_memref(&cond->operand2); + + if (!in_add_address) { + if (is_memref1 && !rc_validate_memref(cond->operand1.value.memref, buffer, sizeof(buffer), console_id, max_address)) { + snprintf(result, result_size, "Condition %d: %s", index, buffer); + return 0; + } + if (is_memref2 && !rc_validate_memref(cond->operand2.value.memref, buffer, sizeof(buffer), console_id, max_address)) { + snprintf(result, result_size, "Condition %d: %s", index, buffer); + return 0; + } + } + else { + in_add_address = 0; + } + + switch (cond->type) { + case RC_CONDITION_ADD_SOURCE: + max = rc_scale_value(max, cond->oper, &cond->operand2); + add_source_max += max; + is_combining = 1; + continue; + + case RC_CONDITION_SUB_SOURCE: + max = rc_scale_value(max, cond->oper, &cond->operand2); + if (add_source_max < max) /* potential underflow - may be expected */ + add_source_max = 0xFFFFFFFF; + is_combining = 1; + continue; + + case RC_CONDITION_ADD_ADDRESS: + if (cond->operand1.type == RC_OPERAND_DELTA || cond->operand1.type == RC_OPERAND_PRIOR) { + snprintf(result, result_size, "Condition %d: Using pointer from previous frame", index); + return 0; + } + in_add_address = 1; + is_combining = 1; + continue; + + case RC_CONDITION_ADD_HITS: + case RC_CONDITION_SUB_HITS: + in_add_hits = 1; + is_combining = 1; + break; + + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + case RC_CONDITION_RESET_NEXT_IF: + is_combining = 1; + break; + + default: + if (in_add_hits) { + if (cond->required_hits == 0) { + snprintf(result, result_size, "Condition %d: Final condition in AddHits chain must have a hit target", index); + return 0; + } + + in_add_hits = 0; + } + + is_combining = 0; + break; + } + + /* if we're in an add source chain, check for overflow */ + if (add_source_max) { + const unsigned long long overflow = add_source_max + max; + if (overflow > 0xFFFFFFFFUL) + max = 0xFFFFFFFF; + else + max += (unsigned)add_source_max; + } + + /* check for comparing two differently sized memrefs */ + max_val = rc_max_value(&cond->operand2); + if (max_val != max && add_source_max == 0 && is_memref1 && is_memref2) { + snprintf(result, result_size, "Condition %d: Comparing different memory sizes", index); + return 0; + } + + /* if either side is a memref, or there's a running add source chain, check for impossible comparisons */ + if (is_memref1 || is_memref2 || add_source_max) { + const size_t prefix_length = snprintf(result, result_size, "Condition %d: ", index); + + unsigned min_val; + switch (cond->operand2.type) { + case RC_OPERAND_CONST: + min_val = cond->operand2.value.num; + break; + + case RC_OPERAND_FP: + min_val = (int)cond->operand2.value.dbl; + + /* cannot compare an integer memory reference to a non-integral floating point value */ + /* assert: is_memref1 (because operand2==FP means !is_memref2) */ + if (!add_source_max && !rc_operand_is_float_memref(&cond->operand1) && + (float)min_val != cond->operand2.value.dbl) { + snprintf(result + prefix_length, result_size - prefix_length, "Comparison is never true"); + return 0; + } + + break; + + default: + min_val = 0; + + /* cannot compare an integer memory reference to a non-integral floating point value */ + /* assert: is_memref2 (because operand1==FP means !is_memref1) */ + if (cond->operand1.type == RC_OPERAND_FP && !add_source_max && !rc_operand_is_float_memref(&cond->operand2) && + (float)((int)cond->operand1.value.dbl) != cond->operand1.value.dbl) { + snprintf(result + prefix_length, result_size - prefix_length, "Comparison is never true"); + return 0; + } + + break; + } + + if (rc_operand_is_float(&cond->operand2) && rc_operand_is_float(&cond->operand1)) { + /* both sides are floats, don't validate range*/ + } else if (!rc_validate_range(min_val, max_val, cond->oper, max, result + prefix_length, result_size - prefix_length)) { + return 0; + } + } + + add_source_max = 0; + } + + if (is_combining) { + snprintf(result, result_size, "Final condition type expects another condition to follow"); + return 0; + } + + *result = '\0'; + return 1; +} + +int rc_validate_condset(const rc_condset_t* condset, char result[], const size_t result_size, unsigned max_address) +{ + return rc_validate_condset_internal(condset, result, result_size, 0, max_address); +} + +int rc_validate_condset_for_console(const rc_condset_t* condset, char result[], const size_t result_size, int console_id) +{ + const unsigned max_address = rc_console_max_address(console_id); + return rc_validate_condset_internal(condset, result, result_size, console_id, max_address); +} + +static int rc_validate_is_combining_condition(const rc_condition_t* condition) +{ + switch (condition->type) + { + case RC_CONDITION_ADD_ADDRESS: + case RC_CONDITION_ADD_HITS: + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + case RC_CONDITION_RESET_NEXT_IF: + case RC_CONDITION_SUB_HITS: + case RC_CONDITION_SUB_SOURCE: + return 1; + + default: + return 0; + } +} + +static const rc_condition_t* rc_validate_next_non_combining_condition(const rc_condition_t* condition) +{ + int is_combining = rc_validate_is_combining_condition(condition); + for (condition = condition->next; condition != NULL; condition = condition->next) + { + if (rc_validate_is_combining_condition(condition)) + is_combining = 1; + else if (is_combining) + is_combining = 0; + else + return condition; + } + + return NULL; +} + +static int rc_validate_get_opposite_comparison(int oper) +{ + switch (oper) + { + case RC_OPERATOR_EQ: return RC_OPERATOR_NE; + case RC_OPERATOR_NE: return RC_OPERATOR_EQ; + case RC_OPERATOR_LT: return RC_OPERATOR_GE; + case RC_OPERATOR_LE: return RC_OPERATOR_GT; + case RC_OPERATOR_GT: return RC_OPERATOR_LE; + case RC_OPERATOR_GE: return RC_OPERATOR_LT; + default: return oper; + } +} + +static const rc_operand_t* rc_validate_get_comparison(const rc_condition_t* condition, int* comparison, unsigned* value) +{ + if (rc_operand_is_memref(&condition->operand1)) + { + if (condition->operand2.type != RC_OPERAND_CONST) + return NULL; + + *comparison = condition->oper; + *value = condition->operand2.value.num; + return &condition->operand1; + } + + if (condition->operand1.type != RC_OPERAND_CONST) + return NULL; + + if (!rc_operand_is_memref(&condition->operand2)) + return NULL; + + *comparison = rc_validate_get_opposite_comparison(condition->oper); + *value = condition->operand1.value.num; + return &condition->operand2; +} + +enum { + RC_OVERLAP_NONE = 0, + RC_OVERLAP_CONFLICTING, + RC_OVERLAP_REDUNDANT, + RC_OVERLAP_DEFER +}; + +static int rc_validate_comparison_overlap(int comparison1, unsigned value1, int comparison2, unsigned value2) +{ + /* NOTE: this only cares if comp2 conflicts with comp1. + * If comp1 conflicts with comp2, we'll catch that later (return RC_OVERLAP_NONE for now) */ + switch (comparison2) + { + case RC_OPERATOR_EQ: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a == 1 | a == 1 && a == 2 | a == 2 && a == 1 */ + /* redundant conflict conflict */ + return (value1 == value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a == 1 | a <= 1 && a == 2 | a <= 2 && a == 1 */ + /* defer conflict defer */ + return (value1 < value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a == 1 | a >= 1 && a == 2 | a >= 2 && a == 1 */ + /* defer defer conflict */ + return (value1 > value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a == 1 | a != 1 && a == 2 | a != 2 && a == 1 */ + /* conflict defer defer */ + return (value1 == value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_LT: /* a < 1 && a == 1 | a < 1 && a == 2 | a < 2 && a == 1 */ + /* conflict conflict defer */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a == 1 | a > 1 && a == 2 | a > 2 && a == 1 */ + /* conflict defer conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + } + break; + + case RC_OPERATOR_NE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a != 1 | a == 1 && a != 2 | a == 2 && a != 1 */ + /* conflict redundant redundant */ + return (value1 == value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_REDUNDANT; + case RC_OPERATOR_LE: /* a <= 1 && a != 1 | a <= 1 && a != 2 | a <= 2 && a != 1 */ + /* none redundant none */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_GE: /* a >= 1 && a != 1 | a >= 1 && a != 2 | a >= 2 && a != 1 */ + /* none none redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a != 1 | a != 1 && a != 2 | a != 2 && a != 1 */ + /* redundant none none */ + return (value1 == value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a != 1 | a < 1 && a != 2 | a < 2 && a != 1 */ + /* redundant redundant none */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a != 1 | a > 1 && a != 2 | a > 2 && a != 1 */ + /* redundant none redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_LT: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a < 1 | a == 1 && a < 2 | a == 2 && a < 1 */ + /* conflict redundant conflict */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a < 1 | a <= 1 && a < 2 | a <= 2 && a < 1 */ + /* defer redundant defer */ + return (value1 < value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a < 1 | a >= 1 && a < 2 | a >= 2 && a < 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a < 1 | a != 1 && a < 2 | a != 2 && a < 1 */ + /* defer none defer */ + return (value1 >= value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a < 1 | a < 1 && a < 2 | a < 2 && a < 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a < 1 | a > 1 && a < 2 | a > 2 && a < 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_LE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a <= 1 | a == 1 && a <= 2 | a == 2 && a <= 1 */ + /* redundant redundant conflict */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a <= 1 | a <= 1 && a <= 2 | a <= 2 && a <= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a <= 1 | a >= 1 && a <= 2 | a >= 2 && a <= 1 */ + /* none none conflict */ + return (value1 > value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_NE: /* a != 1 && a <= 1 | a != 1 && a <= 2 | a != 2 && a <= 1 */ + /* none none defer */ + return (value1 > value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a <= 1 | a < 1 && a <= 2 | a < 2 && a <= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_GT: /* a > 1 && a <= 1 | a > 1 && a <= 2 | a > 2 && a <= 1 */ + /* conflict none conflict */ + return (value1 >= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + } + break; + + case RC_OPERATOR_GT: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a > 1 | a == 1 && a > 2 | a == 2 && a > 1 */ + /* conflict conflict redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a > 1 | a <= 1 && a > 2 | a <= 2 && a > 1 */ + /* conflict conflict defer */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_DEFER; + case RC_OPERATOR_GE: /* a >= 1 && a > 1 | a >= 1 && a > 2 | a >= 2 && a > 1 */ + /* defer defer redundant */ + return (value1 > value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a > 1 | a != 1 && a > 2 | a != 2 && a > 1 */ + /* defer defer none */ + return (value1 <= value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a > 1 | a < 1 && a > 2 | a < 2 && a > 1 */ + /* conflict conflict none */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a > 1 | a > 1 && a > 2 | a > 2 && a > 1 */ + /* redundant defer redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + } + break; + + case RC_OPERATOR_GE: + switch (comparison1) /* comp1 comp2 comp1 comp2 comp1 comp2 */ + { /* value1 = value2 value1 < value2 value1 > value2 */ + case RC_OPERATOR_EQ: /* a == 1 && a >= 1 | a == 1 && a >= 2 | a == 2 && a >= 1 */ + /* redundant conflict redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_CONFLICTING; + case RC_OPERATOR_LE: /* a <= 1 && a >= 1 | a <= 1 && a >= 2 | a <= 2 && a >= 1 */ + /* none conflict none */ + return (value1 < value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GE: /* a >= 1 && a >= 1 | a >= 1 && a >= 2 | a >= 2 && a >= 1 */ + /* redundant redundant defer */ + return (value1 <= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + case RC_OPERATOR_NE: /* a != 1 && a >= 1 | a != 1 && a >= 2 | a != 2 && a >= 1 */ + /* none defer none */ + return (value1 < value2) ? RC_OVERLAP_DEFER : RC_OVERLAP_NONE; + case RC_OPERATOR_LT: /* a < 1 && a >= 1 | a < 1 && a >= 2 | a < 2 && a >= 1 */ + /* conflict conflict none */ + return (value1 <= value2) ? RC_OVERLAP_CONFLICTING : RC_OVERLAP_NONE; + case RC_OPERATOR_GT: /* a > 1 && a >= 1 | a > 1 && a >= 2 | a > 2 && a >= 1 */ + /* redundant defer redundant */ + return (value1 >= value2) ? RC_OVERLAP_REDUNDANT : RC_OVERLAP_DEFER; + } + break; + } + + return RC_OVERLAP_NONE; +} + +static int rc_validate_conflicting_conditions(const rc_condset_t* conditions, const rc_condset_t* compare_conditions, + const char* prefix, const char* compare_prefix, char result[], const size_t result_size) +{ + int comparison1, comparison2; + unsigned value1, value2; + const rc_operand_t* operand1; + const rc_operand_t* operand2; + const rc_condition_t* compare_condition; + const rc_condition_t* condition; + int overlap; + + /* empty group */ + if (conditions == NULL || compare_conditions == NULL) + return 1; + + /* outer loop is the source conditions */ + for (condition = conditions->conditions; condition != NULL; + condition = rc_validate_next_non_combining_condition(condition)) + { + /* hits can be captured at any time, so any potential conflict will not be conflicting at another time */ + if (condition->required_hits) + continue; + + operand1 = rc_validate_get_comparison(condition, &comparison1, &value1); + if (!operand1) + continue; + + switch (condition->type) + { + case RC_CONDITION_PAUSE_IF: + if (conditions != compare_conditions) + break; + /* fallthrough */ + case RC_CONDITION_RESET_IF: + comparison1 = rc_validate_get_opposite_comparison(comparison1); + break; + default: + if (rc_validate_is_combining_condition(condition)) + continue; + break; + } + + /* inner loop is the potentially conflicting conditions */ + for (compare_condition = compare_conditions->conditions; compare_condition != NULL; + compare_condition = rc_validate_next_non_combining_condition(compare_condition)) + { + if (compare_condition == condition) + continue; + + if (compare_condition->required_hits) + continue; + + operand2 = rc_validate_get_comparison(compare_condition, &comparison2, &value2); + if (!operand2 || operand2->value.memref->address != operand1->value.memref->address || + operand2->size != operand1->size || operand2->type != operand1->type) + continue; + + switch (compare_condition->type) + { + case RC_CONDITION_PAUSE_IF: + if (conditions != compare_conditions) + break; + /* fallthrough */ + case RC_CONDITION_RESET_IF: + comparison2 = rc_validate_get_opposite_comparison(comparison2); + break; + default: + if (rc_validate_is_combining_condition(compare_condition)) + continue; + break; + } + + overlap = rc_validate_comparison_overlap(comparison1, value1, comparison2, value2); + switch (overlap) + { + case RC_OVERLAP_CONFLICTING: + if (compare_condition->type == RC_CONDITION_PAUSE_IF || condition->type == RC_CONDITION_PAUSE_IF) + { + /* ignore PauseIf conflicts between groups, unless both conditions are PauseIfs */ + if (conditions != compare_conditions && compare_condition->type != condition->type) + continue; + } + break; + + case RC_OVERLAP_REDUNDANT: + if (prefix != compare_prefix && strcmp(compare_prefix, "Core") == 0) + { + /* if the alt condition is more restrictive than the core condition, ignore it */ + if (rc_validate_comparison_overlap(comparison2, value2, comparison1, value1) != RC_OVERLAP_REDUNDANT) + continue; + } + + if (compare_condition->type == RC_CONDITION_PAUSE_IF || condition->type == RC_CONDITION_PAUSE_IF) + { + /* ignore PauseIf redundancies between groups */ + if (conditions != compare_conditions) + continue; + + /* if the PauseIf is less restrictive than the other condition, it's just a guard. ignore it */ + if (rc_validate_comparison_overlap(comparison2, value2, comparison1, value1) != RC_OVERLAP_REDUNDANT) + continue; + + /* PauseIf redundant with ResetIf is a conflict (both are inverted comparisons) */ + if (compare_condition->type == RC_CONDITION_RESET_IF || condition->type == RC_CONDITION_RESET_IF) + overlap = RC_OVERLAP_CONFLICTING; + } + else if (compare_condition->type == RC_CONDITION_RESET_IF && condition->type != RC_CONDITION_RESET_IF) + { + /* only ever report the redundancy on the non-ResetIf condition. The ResetIf is allowed to + * fire when the non-ResetIf condition is not true. */ + continue; + } + else if (compare_condition->type == RC_CONDITION_MEASURED_IF || condition->type == RC_CONDITION_MEASURED_IF) + { + /* ignore MeasuredIf redundancies between groups */ + if (conditions != compare_conditions) + continue; + + if (compare_condition->type == RC_CONDITION_MEASURED_IF && condition->type != RC_CONDITION_MEASURED_IF) + { + /* only ever report the redundancy on the non-MeasuredIf condition. The MeasuredIf provides + * additional functionality. */ + continue; + } + } + else if (compare_condition->type == RC_CONDITION_TRIGGER || condition->type == RC_CONDITION_TRIGGER) + { + /* Trigger is allowed to be redundant with non-trigger conditions as there may be limits that start a + * challenge that are furhter reduced for the completion of the challenge */ + if (compare_condition->type != condition->type) + continue; + } + break; + + default: + continue; + } + + if (compare_prefix && *compare_prefix) + { + snprintf(result, result_size, "%s Condition %d: %s with %s Condition %d", + compare_prefix, rc_validate_get_condition_index(compare_conditions, compare_condition), + (overlap == RC_OVERLAP_REDUNDANT) ? "Redundant" : "Conflicts", + prefix, rc_validate_get_condition_index(conditions, condition)); + } + else + { + snprintf(result, result_size, "Condition %d: %s with Condition %d", + rc_validate_get_condition_index(compare_conditions, compare_condition), + (overlap == RC_OVERLAP_REDUNDANT) ? "Redundant" : "Conflicts", + rc_validate_get_condition_index(conditions, condition)); + } + return 0; + } + } + + return 1; +} + +static int rc_validate_trigger_internal(const rc_trigger_t* trigger, char result[], const size_t result_size, int console_id, unsigned max_address) +{ + const rc_condset_t* alt; + int index; + + if (!trigger->alternative) + { + if (!rc_validate_condset_internal(trigger->requirement, result, result_size, console_id, max_address)) + return 0; + + return rc_validate_conflicting_conditions(trigger->requirement, trigger->requirement, "", "", result, result_size); + } + + snprintf(result, result_size, "Core "); + if (!rc_validate_condset_internal(trigger->requirement, result + 5, result_size - 5, console_id, max_address)) + return 0; + + /* compare core to itself */ + if (!rc_validate_conflicting_conditions(trigger->requirement, trigger->requirement, "Core", "Core", result, result_size)) + return 0; + + index = 1; + for (alt = trigger->alternative; alt; alt = alt->next, ++index) { + char altname[16]; + const size_t prefix_length = snprintf(result, result_size, "Alt%d ", index); + if (!rc_validate_condset_internal(alt, result + prefix_length, result_size - prefix_length, console_id, max_address)) + return 0; + + /* compare alt to itself */ + snprintf(altname, sizeof(altname), "Alt%d", index); + if (!rc_validate_conflicting_conditions(alt, alt, altname, altname, result, result_size)) + return 0; + + /* compare alt to core */ + if (!rc_validate_conflicting_conditions(trigger->requirement, alt, "Core", altname, result, result_size)) + return 0; + + /* compare core to alt */ + if (!rc_validate_conflicting_conditions(alt, trigger->requirement, altname, "Core", result, result_size)) + return 0; + } + + *result = '\0'; + return 1; +} + +int rc_validate_trigger(const rc_trigger_t* trigger, char result[], const size_t result_size, unsigned max_address) +{ + return rc_validate_trigger_internal(trigger, result, result_size, 0, max_address); +} + +int rc_validate_trigger_for_console(const rc_trigger_t* trigger, char result[], const size_t result_size, int console_id) +{ + const unsigned max_address = rc_console_max_address(console_id); + return rc_validate_trigger_internal(trigger, result, result_size, console_id, max_address); +} diff --git a/dep/rcheevos/src/rcheevos/rc_validate.h b/dep/rcheevos/src/rcheevos/rc_validate.h new file mode 100644 index 000000000..ba7df325e --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_validate.h @@ -0,0 +1,26 @@ +#ifndef RC_VALIDATE_H +#define RC_VALIDATE_H + +#include "rc_runtime_types.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int rc_validate_memrefs(const rc_memref_t* memref, char result[], const size_t result_size, unsigned max_address); + +int rc_validate_condset(const rc_condset_t* condset, char result[], const size_t result_size, unsigned max_address); +int rc_validate_trigger(const rc_trigger_t* trigger, char result[], const size_t result_size, unsigned max_address); + +int rc_validate_memrefs_for_console(const rc_memref_t* memref, char result[], const size_t result_size, int console_id); + +int rc_validate_condset_for_console(const rc_condset_t* condset, char result[], const size_t result_size, int console_id); +int rc_validate_trigger_for_console(const rc_trigger_t* trigger, char result[], const size_t result_size, int console_id); + +#ifdef __cplusplus +} +#endif + +#endif /* RC_VALIDATE_H */ diff --git a/dep/rcheevos/src/rcheevos/rc_version.h b/dep/rcheevos/src/rcheevos/rc_version.h new file mode 100644 index 000000000..cdf857d5f --- /dev/null +++ b/dep/rcheevos/src/rcheevos/rc_version.h @@ -0,0 +1,29 @@ +#ifndef RC_VERSION_H +#define RC_VERSION_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define RCHEEVOS_VERSION_MAJOR 10 +#define RCHEEVOS_VERSION_MINOR 7 +#define RCHEEVOS_VERSION_PATCH 0 + +#define RCHEEVOS_MAKE_VERSION(major, minor, patch) (major * 1000000 + minor * 1000 + patch) +#define RCHEEVOS_VERSION RCHEEVOS_MAKE_VERSION(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR, RCHEEVOS_VERSION_PATCH) + +#define RCHEEVOS_MAKE_STRING(num) #num +#define RCHEEVOS_MAKE_VERSION_STRING(major, minor, patch) RCHEEVOS_MAKE_STRING(major) "." RCHEEVOS_MAKE_STRING(minor) "." RCHEEVOS_MAKE_STRING(patch) +#define RCHEEVOS_MAKE_VERSION_STRING_SHORT(major, minor) RCHEEVOS_MAKE_STRING(major) "." RCHEEVOS_MAKE_STRING(minor) + +#if RCHEEVOS_VERSION_PATCH > 0 + #define RCHEEVOS_VERSION_STRING RCHEEVOS_MAKE_VERSION_STRING(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR, RCHEEVOS_VERSION_PATCH) +#else + #define RCHEEVOS_VERSION_STRING RCHEEVOS_MAKE_VERSION_STRING_SHORT(RCHEEVOS_VERSION_MAJOR, RCHEEVOS_VERSION_MINOR) +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* RC_VERSION_H */ diff --git a/dep/rcheevos/src/rcheevos/richpresence.c b/dep/rcheevos/src/rcheevos/richpresence.c index 68d54490c..3ad6bd991 100644 --- a/dep/rcheevos/src/rcheevos/richpresence.c +++ b/dep/rcheevos/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) { @@ -534,10 +533,11 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, display = nextline; display_line = parse->lines_read; + /* scan as long as we find conditional lines or full line comments */ do { line = nextline; nextline = rc_parse_line(line, &endline, parse); - } while (*line == '?'); + } while (*line == '?' || (line[0] == '/' && line[1] == '/')); } line = nextline; @@ -557,38 +557,48 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, nextline = rc_parse_line(line, &endline, parse); - while (*line == '?') { - /* conditional display: ?trigger?string */ - ptr = ++line; - while (ptr < endline && *ptr != '?') - ++ptr; + do { + if (line[0] == '?') { + /* conditional display: ?trigger?string */ + ptr = ++line; + while (ptr < endline && *ptr != '?') + ++ptr; - if (ptr < endline) { - *nextdisplay = rc_parse_richpresence_display_internal(ptr + 1, endline, parse, firstlookup); - if (parse->offset < 0) - return; - trigger = &((*nextdisplay)->trigger); - rc_parse_trigger_internal(trigger, &line, parse); - trigger->memrefs = 0; - if (parse->offset < 0) - return; - if (parse->buffer) - nextdisplay = &((*nextdisplay)->next); + if (ptr < endline) { + *nextdisplay = rc_parse_richpresence_display_internal(ptr + 1, endline, parse, firstlookup); + if (parse->offset < 0) + return; + trigger = &((*nextdisplay)->trigger); + rc_parse_trigger_internal(trigger, &line, parse); + trigger->memrefs = 0; + if (parse->offset < 0) + return; + if (parse->buffer) + nextdisplay = &((*nextdisplay)->next); + } + } + else if (line[0] != '/' || line[1] != '/') { + break; } line = nextline; nextline = rc_parse_line(line, &endline, parse); - } + } while (1); /* non-conditional display: string */ *nextdisplay = rc_parse_richpresence_display_internal(line, endline, parse, firstlookup); if (*nextdisplay) { hasdisplay = 1; nextdisplay = &((*nextdisplay)->next); - } - /* restore the parser state */ - parse->lines_read = lines_read; + /* restore the parser state */ + parse->lines_read = lines_read; + } + else { + /* this should only happen if the line is blank. + * expect parse->offset to be RC_MISSING_DISPLAY_STRING and leave parse->lines_read + * on the current line for error tracking. */ + } } /* finalize */ diff --git a/dep/rcheevos/src/rcheevos/runtime.c b/dep/rcheevos/src/rcheevos/runtime.c index 32f079a5b..a5a66abd6 100644 --- a/dep/rcheevos/src/rcheevos/runtime.c +++ b/dep/rcheevos/src/rcheevos/runtime.c @@ -9,6 +9,17 @@ #define RC_RICHPRESENCE_DISPLAY_BUFFER_SIZE 256 +rc_runtime_t* rc_runtime_alloc(void) { + rc_runtime_t* self = malloc(sizeof(rc_runtime_t)); + + if (self) { + rc_runtime_init(self); + self->owns_self = 1; + } + + return self; +} + void rc_runtime_init(rc_runtime_t* self) { memset(self, 0, sizeof(rc_runtime_t)); self->next_memref = &self->memrefs; @@ -48,9 +59,13 @@ void rc_runtime_destroy(rc_runtime_t* self) { self->next_memref = 0; self->memrefs = 0; + + if (self->owns_self) { + free(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)); @@ -231,7 +246,7 @@ int rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, unsigned id } if (rc_trigger_state_active(trigger->state)) { - *measured_value = trigger->measured_value; + *measured_value = (trigger->measured_value == RC_MEASURED_UNKNOWN) ? 0 : trigger->measured_value; *measured_target = trigger->measured_target; } else { @@ -257,7 +272,7 @@ int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, unsigned } /* cap the value at the target so we can count past the target: "107 >= 100" */ - value = trigger->measured_value; + value = (trigger->measured_value == RC_MEASURED_UNKNOWN) ? 0 : trigger->measured_value; if (value > trigger->measured_target) value = trigger->measured_target; @@ -534,6 +549,7 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha for (i = self->trigger_count - 1; i >= 0; --i) { rc_trigger_t* trigger = self->triggers[i].trigger; int old_state, new_state; + unsigned old_measured_value; if (!trigger) continue; @@ -552,13 +568,13 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha continue; } + old_measured_value = trigger->measured_value; old_state = trigger->state; new_state = rc_evaluate_trigger(trigger, peek, ud, L); - /* the trigger state doesn't actually change to RESET, RESET just serves as a notification. + /* trigger->state doesn't actually change to RESET, RESET just serves as a notification. * handle the notification, then look at the actual state */ - if (new_state == RC_TRIGGER_STATE_RESET) - { + if (new_state == RC_TRIGGER_STATE_RESET) { runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_RESET; runtime_event.id = self->triggers[i].id; event_handler(&runtime_event); @@ -566,6 +582,32 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha new_state = trigger->state; } + /* if the measured value changed and the achievement hasn't triggered, send a notification */ + if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN && + trigger->measured_target != 0 && trigger->measured_value <= trigger->measured_target && + new_state != RC_TRIGGER_STATE_TRIGGERED && + new_state != RC_TRIGGER_STATE_INACTIVE && new_state != RC_TRIGGER_STATE_WAITING) { + + runtime_event.type = RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED; + runtime_event.id = self->triggers[i].id; + + if (trigger->measured_as_percent) { + /* if reporting measured value as a percentage, only send the notification if the percentage changes */ + unsigned old_percent = (unsigned)(((unsigned long long)old_measured_value * 100) / trigger->measured_target); + unsigned new_percent = (unsigned)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target); + if (old_percent != new_percent) { + runtime_event.value = new_percent; + event_handler(&runtime_event); + } + } + else { + runtime_event.value = trigger->measured_value; + event_handler(&runtime_event); + } + + runtime_event.value = 0; /* achievement loop expects this to stay at 0 */ + } + /* if the state hasn't changed, there won't be any events raised */ if (new_state == old_state) continue; @@ -685,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); @@ -712,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; @@ -725,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/dep/rcheevos/src/rcheevos/runtime_progress.c b/dep/rcheevos/src/rcheevos/runtime_progress.c index cebe981c3..32353faab 100644 --- a/dep/rcheevos/src/rcheevos/runtime_progress.c +++ b/dep/rcheevos/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/dep/rcheevos/src/rcheevos/trigger.c b/dep/rcheevos/src/rcheevos/trigger.c index dda918b53..6061ab8ee 100644 --- a/dep/rcheevos/src/rcheevos/trigger.c +++ b/dep/rcheevos/src/rcheevos/trigger.c @@ -42,8 +42,8 @@ void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_pars *next = 0; *memaddr = aux; - self->measured_value = 0; self->measured_target = parse->measured_target; + self->measured_value = parse->measured_target ? RC_MEASURED_UNKNOWN : 0; self->measured_as_percent = parse->measured_as_percent; self->state = RC_TRIGGER_STATE_WAITING; self->has_hits = 0; @@ -197,14 +197,6 @@ int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* self->measured_value = eval_state.measured_value.value.u32; } - /* if the state is WAITING and the trigger is ready to fire, ignore it and reset the hit counts */ - /* otherwise, if the state is WAITING, proceed to activating the trigger */ - if (self->state == RC_TRIGGER_STATE_WAITING && ret) { - rc_reset_trigger(self); - self->has_hits = 0; - return RC_TRIGGER_STATE_WAITING; - } - /* if any ResetIf condition was true, reset the hit counts */ if (eval_state.was_reset) { /* if the measured value came from a hit count, reset it. do this before calling @@ -247,6 +239,13 @@ int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* is_primed = 0; } else if (ret) { + /* if the state is WAITING and the trigger is ready to fire, ignore it and reset the hit counts */ + if (self->state == RC_TRIGGER_STATE_WAITING) { + rc_reset_trigger(self); + self->has_hits = 0; + return RC_TRIGGER_STATE_WAITING; + } + /* trigger was triggered */ self->state = RC_TRIGGER_STATE_TRIGGERED; return RC_TRIGGER_STATE_TRIGGERED; @@ -284,6 +283,9 @@ void rc_reset_trigger(rc_trigger_t* self) { rc_reset_trigger_hitcounts(self); self->state = RC_TRIGGER_STATE_WAITING; - self->measured_value = 0; + + if (self->measured_target) + self->measured_value = RC_MEASURED_UNKNOWN; + self->has_hits = 0; } diff --git a/dep/rcheevos/src/rcheevos/value.c b/dep/rcheevos/src/rcheevos/value.c index 70444c104..54784caab 100644 --- a/dep/rcheevos/src/rcheevos/value.c +++ b/dep/rcheevos/src/rcheevos/value.c @@ -111,6 +111,7 @@ void rc_parse_legacy_value(rc_value_t* self, const char** memaddr, rc_parse_stat case RC_OPERATOR_MULT: case RC_OPERATOR_DIV: case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: case RC_OPERATOR_NONE: break; @@ -284,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; @@ -427,6 +442,26 @@ static rc_typed_value_t* rc_typed_value_convert_into(rc_typed_value_t* dest, con return dest; } +void rc_typed_value_negate(rc_typed_value_t* value) { + switch (value->type) + { + case RC_VALUE_TYPE_UNSIGNED: + rc_typed_value_convert(value, RC_VALUE_TYPE_SIGNED); + /* fallthrough to RC_VALUE_TYPE_SIGNED */ + + case RC_VALUE_TYPE_SIGNED: + value->value.i32 = -(value->value.i32); + break; + + case RC_VALUE_TYPE_FLOAT: + value->value.f32 = -(value->value.f32); + break; + + default: + break; + } +} + void rc_typed_value_add(rc_typed_value_t* value, const rc_typed_value_t* amount) { rc_typed_value_t converted; diff --git a/dep/rcheevos/src/rhash/cdreader.c b/dep/rcheevos/src/rhash/cdreader.c index 9f464b1bb..c0f5c88a2 100644 --- a/dep/rcheevos/src/rhash/cdreader.c +++ b/dep/rcheevos/src/rhash/cdreader.c @@ -22,6 +22,7 @@ struct cdrom_t void* file_handle; /* the file handle for reading the track data */ int sector_size; /* the size of each sector in the track data */ int sector_header_size; /* the offset to the raw data within a sector block */ + int raw_data_size; /* the amount of raw data within a sector block */ int64_t file_track_offset;/* the offset of the track data within the file */ int track_first_sector; /* the first absolute sector associated to the track (includes pregap) */ int track_pregap_sectors; /* the number of pregap sectors */ @@ -58,6 +59,7 @@ static void cdreader_determine_sector_size(struct cdrom_t* cdrom) cdrom->sector_size = 0; cdrom->sector_header_size = 0; + cdrom->raw_data_size = 2048; rc_file_seek(cdrom->file_handle, toc_sector * 2352 + cdrom->file_track_offset, SEEK_SET); if (rc_file_read(cdrom->file_handle, header, sizeof(header)) < sizeof(header)) @@ -122,6 +124,8 @@ static void* cdreader_open_bin_track(const char* path, uint32_t track) return NULL; cdrom = (struct cdrom_t*)calloc(1, sizeof(*cdrom)); + if (!cdrom) + return NULL; cdrom->file_handle = file_handle; #ifndef NDEBUG cdrom->track_id = track; @@ -219,6 +223,7 @@ static int cdreader_open_bin(struct cdrom_t* cdrom, const char* path, const char { cdrom->sector_size = 2352; cdrom->sector_header_size = 0; + cdrom->raw_data_size = 2352; /* no header or footer data on audio tracks */ } } @@ -282,6 +287,7 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) char* bin_filename = NULL; char *ptr, *ptr2, *end; int done = 0; + int session = 1; size_t num_read = 0; struct cdrom_t* cdrom = NULL; @@ -339,7 +345,7 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) ++ptr; /* convert mm:ss:ff to sector count */ - sscanf(ptr, "%d:%d:%d", &m, &s, &f); + sscanf_s(ptr, "%d:%d:%d", &m, &s, &f); sector_offset = ((m * 60) + s) * 75 + f; if (current_track.first_sector == -1) @@ -389,6 +395,13 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) done = 1; break; } + + if (track == RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION && session == 2) + { + track = current_track.id; + done = 1; + break; + } } } else if (strncasecmp(ptr, "TRACK ", 6) == 0) @@ -465,6 +478,20 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) if (ptr2 - ptr < (int)sizeof(current_track.filename)) memcpy(current_track.filename, ptr, ptr2 - ptr); } + else if (strncasecmp(ptr, "REM ", 4) == 0) + { + ptr += 4; + while (*ptr == ' ') + ++ptr; + + if (strncasecmp(ptr, "SESSION ", 8) == 0) + { + ptr += 8; + while (*ptr == ' ') + ++ptr; + session = atoi(ptr); + } + } while (*ptr && *ptr != '\n') ++ptr; @@ -694,8 +721,8 @@ static void* cdreader_open_gdi_track(const char* path, uint32_t track) largest_track_size = track_size; largest_track = current_track; largest_track_lba = lba; - strcpy(largest_track_file, file); - strcpy(largest_track_sector_size, sector_size); + strcpy_s(largest_track_file, sizeof(largest_track_file), file); + strcpy_s(largest_track_sector_size, sizeof(largest_track_sector_size), sector_size); } } } @@ -723,8 +750,8 @@ static void* cdreader_open_gdi_track(const char* path, uint32_t track) if (largest_track != 0 && largest_track != current_track) { current_track = largest_track; - strcpy(file, largest_track_file); - strcpy(sector_size, largest_track_sector_size); + strcpy_s(file, sizeof(file), largest_track_file); + strcpy_s(sector_size, sizeof(sector_size), largest_track_sector_size); lba = largest_track_lba; } @@ -794,18 +821,18 @@ static size_t cdreader_read_sector(void* track_handle, uint32_t sector, void* bu sector_start = (int64_t)(sector - cdrom->track_first_sector) * cdrom->sector_size + cdrom->sector_header_size + cdrom->file_track_offset; - while (requested_bytes > 2048) + while (requested_bytes > (size_t)cdrom->raw_data_size) { rc_file_seek(cdrom->file_handle, sector_start, SEEK_SET); - num_read = rc_file_read(cdrom->file_handle, buffer_ptr, 2048); + num_read = rc_file_read(cdrom->file_handle, buffer_ptr, cdrom->raw_data_size); total_read += num_read; - if (num_read < 2048) + if (num_read < (size_t)cdrom->raw_data_size) return total_read; - buffer_ptr += 2048; + buffer_ptr += cdrom->raw_data_size; sector_start += cdrom->sector_size; - requested_bytes -= 2048; + requested_bytes -= cdrom->raw_data_size; } rc_file_seek(cdrom->file_handle, sector_start, SEEK_SET); @@ -844,7 +871,7 @@ void rc_hash_get_default_cdreader(struct rc_hash_cdreader* cdreader) cdreader->first_track_sector = cdreader_first_track_sector; } -void rc_hash_init_default_cdreader() +void rc_hash_init_default_cdreader(void) { struct rc_hash_cdreader cdreader; rc_hash_get_default_cdreader(&cdreader); diff --git a/dep/rcheevos/src/rhash/hash.c b/dep/rcheevos/src/rhash/hash.c index 1c983146b..e93423b86 100644 --- a/dep/rcheevos/src/rhash/hash.c +++ b/dep/rcheevos/src/rhash/hash.c @@ -48,7 +48,13 @@ static struct rc_hash_filereader* filereader = NULL; static void* filereader_open(const char* path) { +#if defined(__STDC_WANT_SECURE_LIB__) + FILE* fp; + fopen_s(&fp, path, "rb"); + return fp; +#else return fopen(path, "rb"); +#endif } static void filereader_seek(void* file_handle, int64_t offset, int origin) @@ -223,12 +229,17 @@ static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uns { uint8_t buffer[2048], *tmp; int sector; + unsigned num_sectors = 0; size_t filename_length; const char* slash; if (!track_handle) return 0; + /* we start at the root. don't need to explicitly find it */ + if (*path == '\\') + ++path; + filename_length = strlen(path); slash = strrchr(path, '\\'); if (slash) @@ -247,6 +258,8 @@ static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uns } else { + unsigned logical_block_size; + /* find the cd information */ if (!rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle) + 16, buffer, 256)) return 0; @@ -255,6 +268,15 @@ static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uns * https://www.cdroller.com/htm/readdata.html */ sector = buffer[156 + 2] | (buffer[156 + 3] << 8) | (buffer[156 + 4] << 16); + + /* if the table of contents spans more than one sector, it's length of section will exceed it's logical block size */ + logical_block_size = (buffer[128] | (buffer[128 + 1] << 8)); /* logical block size */ + if (logical_block_size == 0) { + num_sectors = 1; + } else { + num_sectors = (buffer[156 + 10] | (buffer[156 + 11] << 8) | (buffer[156 + 12] << 16) | (buffer[156 + 13] << 24)); /* length of section */ + num_sectors /= logical_block_size; + } } /* fetch and process the directory record */ @@ -262,21 +284,34 @@ static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uns return 0; tmp = buffer; - while (tmp < buffer + sizeof(buffer)) + do { - if (!*tmp) - return 0; + if (tmp >= buffer + sizeof(buffer) || !*tmp) + { + /* end of this path table block. if the path table spans multiple sectors, keep scanning */ + if (num_sectors > 1) + { + --num_sectors; + if (rc_cd_read_sector(track_handle, ++sector, buffer, sizeof(buffer))) + { + tmp = buffer; + continue; + } + } + break; + } /* filename is 33 bytes into the record and the format is "FILENAME;version" or "DIRECTORY" */ - if ((tmp[33 + filename_length] == ';' || tmp[33 + filename_length] == '\0') && + if ((tmp[32] == filename_length || tmp[33 + filename_length] == ';') && strncasecmp((const char*)(tmp + 33), path, filename_length) == 0) { sector = tmp[2] | (tmp[3] << 8) | (tmp[4] << 16); if (verbose_message_callback) { - snprintf((char*)buffer, sizeof(buffer), "Found %s at sector %d", path, sector); - verbose_message_callback((const char*)buffer); + char message[128]; + snprintf(message, sizeof(message), "Found %s at sector %d", path, sector); + verbose_message_callback(message); } if (size) @@ -287,7 +322,7 @@ static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uns /* the first byte of the record is the length of the record */ tmp += *tmp; - } + } while (1); return 0; } @@ -347,6 +382,34 @@ int rc_path_compare_extension(const char* path, const char* ext) /* ===================================================== */ +static void rc_hash_byteswap16(uint8_t* buffer, const uint8_t* stop) +{ + uint32_t* ptr = (uint32_t*)buffer; + const uint32_t* stop32 = (const uint32_t*)stop; + while (ptr < stop32) + { + uint32_t temp = *ptr; + temp = (temp & 0xFF00FF00) >> 8 | + (temp & 0x00FF00FF) << 8; + *ptr++ = temp; + } +} + +static void rc_hash_byteswap32(uint8_t* buffer, const uint8_t* stop) +{ + uint32_t* ptr = (uint32_t*)buffer; + const uint32_t* stop32 = (const uint32_t*)stop; + while (ptr < stop32) + { + uint32_t temp = *ptr; + temp = (temp & 0xFF000000) >> 24 | + (temp & 0x00FF0000) >> 8 | + (temp & 0x0000FF00) << 8 | + (temp & 0x000000FF) << 24; + *ptr++ = temp; + } +} + static int rc_hash_finalize(md5_state_t* md5, char hash[33]) { md5_byte_t digest[16]; @@ -415,6 +478,9 @@ static int rc_hash_cd_file(md5_state_t* md5, void* track_handle, uint32_t sector verbose_message_callback(message); } + if (size < (unsigned)num_read) + size = (unsigned)num_read; + do { md5_append(md5, buffer, (int)num_read); @@ -684,6 +750,142 @@ static int rc_hash_text(char hash[33], const uint8_t* buffer, size_t buffer_size return rc_hash_finalize(&md5, hash); } +/* helper variable only used for testing */ +const char* _rc_hash_jaguar_cd_homebrew_hash = NULL; + +static int rc_hash_jaguar_cd(char hash[33], const char* path) +{ + uint8_t buffer[2352]; + char message[128]; + void* track_handle; + md5_state_t md5; + int byteswapped = 0; + unsigned size = 0; + unsigned offset = 0; + unsigned sector = 0; + unsigned remaining; + unsigned i; + + /* Jaguar CD header is in the first sector of the first data track OF THE SECOND SESSION. + * The first track must be an audio track, but may be a warning message or actual game audio */ + track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* The header is an unspecified distance into the first sector, but usually two bytes in. + * It consists of 64 bytes of "TAIR" or "ATRI" repeating, depending on whether or not the data + * is byteswapped. Then another 32 byte that reads "ATARI APPROVED DATA HEADER ATRI " + * (possibly byteswapped). Then a big-endian 32-bit value for the address where the boot code + * should be loaded, and a second big-endian 32-bit value for the size of the boot code. */ + sector = rc_cd_first_track_sector(track_handle); + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + + for (i = 64; i < sizeof(buffer) - 32 - 4 * 3; i++) + { + if (memcmp(&buffer[i], "TARA IPARPVODED TA AEHDAREA RT I", 32) == 0) + { + byteswapped = 1; + offset = i + 32 + 4; + size = (buffer[offset] << 16) | (buffer[offset + 1] << 24) | (buffer[offset + 2]) | (buffer[offset + 3] << 8); + break; + } + else if (memcmp(&buffer[i], "ATARI APPROVED DATA HEADER ATRI ", 32) == 0) + { + byteswapped = 0; + offset = i + 32 + 4; + size = (buffer[offset] << 24) | (buffer[offset + 1] << 16) | (buffer[offset + 2] << 8) | (buffer[offset + 3]); + break; + } + } + + if (size == 0) /* did not see ATARI APPROVED DATA HEADER */ + { + rc_cd_close_track(track_handle); + return rc_hash_error("Not a Jaguar CD"); + } + + i = 0; /* only loop once */ + do + { + md5_init(&md5); + + offset += 4; + + if (verbose_message_callback) + { + snprintf(message, sizeof(message), "Hashing boot executable (%u bytes starting at %u bytes into sector %u)", size, offset, sector); + rc_hash_verbose(message); + } + + if (size > MAX_BUFFER_SIZE) + size = MAX_BUFFER_SIZE; + + do + { + if (byteswapped) + rc_hash_byteswap16(buffer, &buffer[sizeof(buffer)]); + + remaining = sizeof(buffer) - offset; + if (remaining >= size) + { + md5_append(&md5, &buffer[offset], size); + size = 0; + break; + } + + md5_append(&md5, &buffer[offset], remaining); + size -= remaining; + offset = 0; + } while (rc_cd_read_sector(track_handle, ++sector, buffer, sizeof(buffer)) == sizeof(buffer)); + + rc_cd_close_track(track_handle); + + if (size > 0) + return rc_hash_error("Not enough data"); + + rc_hash_finalize(&md5, hash); + + /* homebrew games all seem to have the same boot executable and store the actual game code in track 2. + * if we generated something other than the homebrew hash, return it. assume all homebrews are byteswapped. */ + if (strcmp(hash, "254487b59ab21bc005338e85cbf9fd2f") != 0 || !byteswapped) { + if (_rc_hash_jaguar_cd_homebrew_hash == NULL || strcmp(hash, _rc_hash_jaguar_cd_homebrew_hash) != 0) + return 1; + } + + /* if we've already been through the loop a second time, just return the hash */ + if (i == 1) + return 1; + ++i; + + if (verbose_message_callback) + { + snprintf(message, sizeof(message), "Potential homebrew at sector %u, checking for KART data in track 2", sector); + rc_hash_verbose(message); + } + + track_handle = rc_cd_open_track(path, 2); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* track 2 of the homebrew code has the 64 bytes or ATRI followed by 32 bytes of "ATARI APPROVED DATA HEADER ATRI!", + * then 64 bytes of KART repeating. */ + sector = rc_cd_first_track_sector(track_handle); + rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); + if (memcmp(&buffer[0x5E], "RT!IRTKA", 8) != 0) + return rc_hash_error("Homebrew executable not found in track 2"); + + /* found KART data*/ + if (verbose_message_callback) + { + snprintf(message, sizeof(message), "Found KART data in track 2"); + rc_hash_verbose(message); + } + + offset = 0xA6; + size = (buffer[offset] << 16) | (buffer[offset + 1] << 24) | (buffer[offset + 2]) | (buffer[offset + 3] << 8); + } while (1); +} + static int rc_hash_lynx(char hash[33], const uint8_t* buffer, size_t buffer_size) { /* if the file contains a header, ignore it */ @@ -698,6 +900,70 @@ static int rc_hash_lynx(char hash[33], const uint8_t* buffer, size_t buffer_size return rc_hash_buffer(hash, buffer, buffer_size); } +static int rc_hash_neogeo_cd(char hash[33], const char* path) +{ + char buffer[1024], *ptr; + void* track_handle; + uint32_t sector; + unsigned size; + md5_state_t md5; + + track_handle = rc_cd_open_track(path, 1); + if (!track_handle) + return rc_hash_error("Could not open track"); + + /* https://wiki.neogeodev.org/index.php?title=IPL_file, https://wiki.neogeodev.org/index.php?title=PRG_file + * IPL file specifies data to be loaded before the game starts. PRG files are the executable code + */ + sector = rc_cd_find_file_sector(track_handle, "IPL.TXT", &size); + if (!sector) + { + rc_cd_close_track(track_handle); + return rc_hash_error("Not a NeoGeo CD game disc"); + } + + if (rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)) == 0) + { + rc_cd_close_track(track_handle); + return 0; + } + + md5_init(&md5); + + buffer[sizeof(buffer) - 1] = '\0'; + ptr = &buffer[0]; + do + { + char* start = ptr; + while (*ptr && *ptr != '.') + ++ptr; + + if (strncasecmp(ptr, ".PRG", 4) == 0) + { + ptr += 4; + *ptr++ = '\0'; + + sector = rc_cd_find_file_sector(track_handle, start, &size); + if (!sector || !rc_hash_cd_file(&md5, track_handle, sector, NULL, size, start)) + { + char error[128]; + rc_cd_close_track(track_handle); + snprintf(error, sizeof(error), "Could not read %.16s", start); + return rc_hash_error(error); + } + } + + while (*ptr && *ptr != '\n') + ++ptr; + if (*ptr != '\n') + break; + ++ptr; + } while (*ptr != '\0' && *ptr != '\x1a'); + + rc_cd_close_track(track_handle); + return rc_hash_finalize(&md5, hash); +} + static int rc_hash_nes(char hash[33], const uint8_t* buffer, size_t buffer_size) { /* if the file contains a header, ignore it */ @@ -719,34 +985,6 @@ static int rc_hash_nes(char hash[33], const uint8_t* buffer, size_t buffer_size) return rc_hash_buffer(hash, buffer, buffer_size); } -static void rc_hash_v64_to_z64(uint8_t* buffer, const uint8_t* stop) -{ - uint32_t* ptr = (uint32_t*)buffer; - const uint32_t* stop32 = (const uint32_t*)stop; - while (ptr < stop32) - { - uint32_t temp = *ptr; - temp = (temp & 0xFF00FF00) >> 8 | - (temp & 0x00FF00FF) << 8; - *ptr++ = temp; - } -} - -static void rc_hash_n64_to_z64(uint8_t* buffer, const uint8_t* stop) -{ - uint32_t* ptr = (uint32_t*)buffer; - const uint32_t* stop32 = (const uint32_t*)stop; - while (ptr < stop32) - { - uint32_t temp = *ptr; - temp = (temp & 0xFF000000) >> 24 | - (temp & 0x00FF0000) >> 8 | - (temp & 0x0000FF00) << 8 | - (temp & 0x000000FF) << 24; - *ptr++ = temp; - } -} - static int rc_hash_n64(char hash[33], const char* path) { uint8_t* buffer; @@ -787,6 +1025,9 @@ static int rc_hash_n64(char hash[33], const char* path) rc_hash_verbose("converting n64 to z64"); is_n64 = 1; } + else if (buffer[0] == 0xE8 || buffer[0] == 0x22) /* ndd format (don't byteswap) */ + { + } else { free(buffer); @@ -818,9 +1059,9 @@ static int rc_hash_n64(char hash[33], const char* path) rc_file_read(file_handle, buffer, (int)buffer_size); if (is_v64) - rc_hash_v64_to_z64(buffer, stop); + rc_hash_byteswap16(buffer, stop); else if (is_n64) - rc_hash_n64_to_z64(buffer, stop); + rc_hash_byteswap32(buffer, stop); md5_append(&md5, buffer, (int)buffer_size); remaining -= buffer_size; @@ -832,9 +1073,9 @@ static int rc_hash_n64(char hash[33], const char* path) stop = buffer + remaining; if (is_v64) - rc_hash_v64_to_z64(buffer, stop); + rc_hash_byteswap16(buffer, stop); else if (is_n64) - rc_hash_n64_to_z64(buffer, stop); + rc_hash_byteswap32(buffer, stop); md5_append(&md5, buffer, (int)remaining); } @@ -957,6 +1198,121 @@ static int rc_hash_nintendo_ds(char hash[33], const char* path) return rc_hash_finalize(&md5, hash); } +static int rc_hash_gamecube(char hash[33], const char* path) +{ + md5_state_t md5; + void* file_handle; + const uint32_t BASE_HEADER_SIZE = 0x2440; + const uint32_t MAX_HEADER_SIZE = 1024 * 1024; + uint32_t apploader_header_size, apploader_body_size, apploader_trailer_size, header_size; + uint8_t quad_buffer[4]; + uint8_t addr_buffer[0xD8]; + uint8_t* buffer; + uint32_t dol_offset; + uint32_t dol_offsets[18]; + uint32_t dol_sizes[18]; + uint32_t dol_buf_size = 0; + uint32_t ix; + + file_handle = rc_file_open(path); + if (!file_handle) + return rc_hash_error("Could not open file"); + + /* Verify Gamecube */ + rc_file_seek(file_handle, 0x1c, SEEK_SET); + rc_file_read(file_handle, quad_buffer, 4); + if (quad_buffer[0] != 0xC2|| quad_buffer[1] != 0x33 || quad_buffer[2] != 0x9F || quad_buffer[3] != 0x3D) + { + rc_file_close(file_handle); + return rc_hash_error("Not a Gamecube disc"); + } + + /* GetApploaderSize */ + rc_file_seek(file_handle, BASE_HEADER_SIZE + 0x14, SEEK_SET); + apploader_header_size = 0x20; + rc_file_read(file_handle, quad_buffer, 4); + apploader_body_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + rc_file_read(file_handle, quad_buffer, 4); + apploader_trailer_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + header_size = BASE_HEADER_SIZE + apploader_header_size + apploader_body_size + apploader_trailer_size; + if (header_size > MAX_HEADER_SIZE) header_size = MAX_HEADER_SIZE; + + /* Hash headers */ + buffer = (uint8_t*)malloc(header_size); + if (!buffer) + { + rc_file_close(file_handle); + return rc_hash_error("Could not allocate temporary buffer"); + } + rc_file_seek(file_handle, 0, SEEK_SET); + rc_file_read(file_handle, buffer, header_size); + md5_init(&md5); + if (verbose_message_callback) + { + char message[128]; + snprintf(message, sizeof(message), "Hashing %u byte header", header_size); + verbose_message_callback(message); + } + md5_append(&md5, buffer, header_size); + + /* GetBootDOLOffset + * Base header size is guaranteed larger than 0x423 therefore buffer contains dol_offset right now + */ + dol_offset = (buffer[0x420] << 24) | (buffer[0x421] << 16) | (buffer[0x422] << 8) | buffer[0x423]; + free(buffer); + + /* Find offsetsand sizes for the 7 main.dol code segments and 11 main.dol data segments */ + rc_file_seek(file_handle, dol_offset, SEEK_SET); + rc_file_read(file_handle, addr_buffer, 0xD8); + for (ix = 0; ix < 18; ix++) + { + dol_offsets[ix] = + (addr_buffer[0x0 + ix * 4] << 24) | + (addr_buffer[0x1 + ix * 4] << 16) | + (addr_buffer[0x2 + ix * 4] << 8) | + addr_buffer[0x3 + ix * 4]; + dol_sizes[ix] = + (addr_buffer[0x90 + ix * 4] << 24) | + (addr_buffer[0x91 + ix * 4] << 16) | + (addr_buffer[0x92 + ix * 4] << 8) | + addr_buffer[0x93 + ix * 4]; + dol_buf_size = (dol_sizes[ix] > dol_buf_size) ? dol_sizes[ix] : dol_buf_size; + } + + /* Iterate through the 18 main.dol segments and hash each */ + buffer = (uint8_t*)malloc(dol_buf_size); + if (!buffer) + { + rc_file_close(file_handle); + return rc_hash_error("Could not allocate temporary buffer"); + } + for (ix = 0; ix < 18; ix++) + { + if (dol_sizes[ix] == 0) + continue; + rc_file_seek(file_handle, dol_offsets[ix], SEEK_SET); + rc_file_read(file_handle, buffer, dol_sizes[ix]); + if (verbose_message_callback) + { + char message[128]; + if (ix < 7) + snprintf(message, sizeof(message), "Hashing %u byte main.dol code segment %u", dol_sizes[ix], ix); + else + snprintf(message, sizeof(message), "Hashing %u byte main.dol data segment %u", dol_sizes[ix], ix - 7); + verbose_message_callback(message); + } + md5_append(&md5, buffer, dol_sizes[ix]); + } + + /* Finalize */ + rc_file_close(file_handle); + free(buffer); + + return rc_hash_finalize(&md5, hash); +} + static int rc_hash_pce(char hash[33], const uint8_t* buffer, size_t buffer_size) { /* if the file contains a header, ignore it (expect ROM data to be multiple of 128KB) */ @@ -1290,7 +1646,7 @@ static int rc_hash_find_playstation_executable(void* track_handle, const char* b if (strncmp(ptr, cdrom_prefix, cdrom_prefix_len) == 0) ptr += cdrom_prefix_len; - if (*ptr == '\\') + while (*ptr == '\\') ++ptr; start = ptr; @@ -1454,18 +1810,30 @@ static int rc_hash_psp(char hash[33], const char* path) */ sector = rc_cd_find_file_sector(track_handle, "PSP_GAME\\PARAM.SFO", &size); if (!sector) + { + rc_cd_close_track(track_handle); return rc_hash_error("Not a PSP game disc"); + } md5_init(&md5); if (!rc_hash_cd_file(&md5, track_handle, sector, NULL, size, "PSP_GAME\\PARAM.SFO")) + { + rc_cd_close_track(track_handle); return 0; + } sector = rc_cd_find_file_sector(track_handle, "PSP_GAME\\SYSDIR\\EBOOT.BIN", &size); if (!sector) + { + rc_cd_close_track(track_handle); return rc_hash_error("Could not find primary executable"); + } if (!rc_hash_cd_file(&md5, track_handle, sector, NULL, size, "PSP_GAME\\SYSDIR\\EBOOT.BIN")) + { + rc_cd_close_track(track_handle); return 0; + } rc_cd_close_track(track_handle); return rc_hash_finalize(&md5, hash); @@ -1527,7 +1895,11 @@ static struct rc_buffered_file rc_buffered_file; static void* rc_file_open_buffered_file(const char* path) { struct rc_buffered_file* handle = (struct rc_buffered_file*)malloc(sizeof(struct rc_buffered_file)); - memcpy(handle, &rc_buffered_file, sizeof(rc_buffered_file)); + (void)path; + + if (handle) + memcpy(handle, &rc_buffered_file, sizeof(rc_buffered_file)); + return handle; } @@ -1631,7 +2003,9 @@ int rc_hash_generate_from_buffer(char hash[33], int console_id, const uint8_t* b case RC_CONSOLE_SEGA_32X: case RC_CONSOLE_SG1000: case RC_CONSOLE_SUPERVISION: + case RC_CONSOLE_TI83: case RC_CONSOLE_TIC80: + case RC_CONSOLE_UZEBOX: case RC_CONSOLE_VECTREX: case RC_CONSOLE_VIRTUAL_BOY: case RC_CONSOLE_WASM4: @@ -1659,6 +2033,7 @@ int rc_hash_generate_from_buffer(char hash[33], int console_id, const uint8_t* b case RC_CONSOLE_NINTENDO_64: case RC_CONSOLE_NINTENDO_DS: + case RC_CONSOLE_NINTENDO_DSI: return rc_hash_file_from_buffer(hash, console_id, buffer, buffer_size); } } @@ -1922,7 +2297,9 @@ int rc_hash_generate_from_file(char hash[33], int console_id, const char* path) case RC_CONSOLE_SEGA_32X: case RC_CONSOLE_SG1000: case RC_CONSOLE_SUPERVISION: + case RC_CONSOLE_TI83: case RC_CONSOLE_TIC80: + case RC_CONSOLE_UZEBOX: case RC_CONSOLE_VECTREX: case RC_CONSOLE_VIRTUAL_BOY: case RC_CONSOLE_WASM4: @@ -1946,6 +2323,7 @@ int rc_hash_generate_from_file(char hash[33], int console_id, const char* path) case RC_CONSOLE_ATARI_7800: case RC_CONSOLE_ATARI_LYNX: case RC_CONSOLE_NINTENDO: + case RC_CONSOLE_PC_ENGINE: case RC_CONSOLE_SUPER_NINTENDO: /* additional logic whole-file hash - buffer then call rc_hash_generate_from_buffer */ return rc_hash_buffered_file(hash, console_id, path); @@ -1959,13 +2337,29 @@ int rc_hash_generate_from_file(char hash[33], int console_id, const char* path) case RC_CONSOLE_ARCADE: return rc_hash_arcade(hash, path); + case RC_CONSOLE_ATARI_JAGUAR_CD: + return rc_hash_jaguar_cd(hash, path); + + case RC_CONSOLE_DREAMCAST: + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, path); + + return rc_hash_dreamcast(hash, path); + + case RC_CONSOLE_GAMECUBE: + return rc_hash_gamecube(hash, path); + + case RC_CONSOLE_NEO_GEO_CD: + return rc_hash_neogeo_cd(hash, path); + case RC_CONSOLE_NINTENDO_64: return rc_hash_n64(hash, path); case RC_CONSOLE_NINTENDO_DS: + case RC_CONSOLE_NINTENDO_DSI: return rc_hash_nintendo_ds(hash, path); - case RC_CONSOLE_PC_ENGINE: + case RC_CONSOLE_PC_ENGINE_CD: if (rc_path_compare_extension(path, "cue") || rc_path_compare_extension(path, "chd")) return rc_hash_pce_cd(hash, path); @@ -1995,12 +2389,6 @@ int rc_hash_generate_from_file(char hash[33], int console_id, const char* path) case RC_CONSOLE_PSP: return rc_hash_psp(hash, path); - case RC_CONSOLE_DREAMCAST: - if (rc_path_compare_extension(path, "m3u")) - return rc_hash_generate_from_playlist(hash, console_id, path); - - return rc_hash_dreamcast(hash, path); - case RC_CONSOLE_SEGA_CD: case RC_CONSOLE_SATURN: if (rc_path_compare_extension(path, "m3u")) @@ -2077,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; @@ -2108,6 +2496,15 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } break; + case '8': + /* http://tibasicdev.wikidot.com/file-extensions */ + if (rc_path_compare_extension(ext, "83g") || + rc_path_compare_extension(ext, "83p")) + { + iterator->consoles[0] = RC_CONSOLE_TI83; + } + break; + case 'a': if (rc_path_compare_extension(ext, "a78")) { @@ -2162,9 +2559,11 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* iterator->consoles[1] = RC_CONSOLE_PLAYSTATION_2; iterator->consoles[2] = RC_CONSOLE_DREAMCAST; iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ - iterator->consoles[4] = RC_CONSOLE_PC_ENGINE; + iterator->consoles[4] = RC_CONSOLE_PC_ENGINE_CD; iterator->consoles[5] = RC_CONSOLE_3DO; iterator->consoles[6] = RC_CONSOLE_PCFX; + iterator->consoles[7] = RC_CONSOLE_NEO_GEO_CD; + iterator->consoles[8] = RC_CONSOLE_ATARI_JAGUAR_CD; need_path = 1; } else if (rc_path_compare_extension(ext, "chd")) @@ -2173,7 +2572,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* iterator->consoles[1] = RC_CONSOLE_PLAYSTATION_2; iterator->consoles[2] = RC_CONSOLE_DREAMCAST; iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ - iterator->consoles[4] = RC_CONSOLE_PC_ENGINE; + iterator->consoles[4] = RC_CONSOLE_PC_ENGINE_CD; iterator->consoles[5] = RC_CONSOLE_3DO; iterator->consoles[6] = RC_CONSOLE_PCFX; need_path = 1; @@ -2199,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")) { @@ -2219,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; @@ -2330,7 +2729,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } else if (rc_path_compare_extension(ext, "nds")) { - iterator->consoles[0] = RC_CONSOLE_NINTENDO_DS; + iterator->consoles[0] = RC_CONSOLE_NINTENDO_DS; /* ASSERT: handles both DS and DSi */ } else if (rc_path_compare_extension(ext, "n64") || rc_path_compare_extension(ext, "ndd")) @@ -2412,6 +2811,13 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* } break; + case 'u': + if (rc_path_compare_extension(ext, "uze")) + { + iterator->consoles[0] = RC_CONSOLE_UZEBOX; + } + break; + case 'v': if (rc_path_compare_extension(ext, "vb")) { diff --git a/dep/rcheevos/src/rhash/md5.c b/dep/rcheevos/src/rhash/md5.c index c35d96c5e..f3a520566 100644 --- a/dep/rcheevos/src/rhash/md5.c +++ b/dep/rcheevos/src/rhash/md5.c @@ -52,6 +52,7 @@ */ #include "md5.h" +#include #include #undef BYTE_ORDER /* 1 = big-endian, -1 = little-endian, 0 = unknown */ @@ -161,7 +162,7 @@ md5_process(md5_state_t *pms, const md5_byte_t *data /*[64]*/) * On little-endian machines, we can process properly aligned * data without copying it. */ - if (!((data - (const md5_byte_t *)0) & 3)) { + if (!((ptrdiff_t)data & 3)) { /* data are properly aligned */ X = (const md5_word_t *)data; } else {