diff --git a/Makefile.common b/Makefile.common index d6668ecf09..35b8c83c43 100644 --- a/Makefile.common +++ b/Makefile.common @@ -2385,6 +2385,10 @@ ifeq ($(HAVE_NETWORKING), 1) deps/rcheevos/src/rhash/aes.o \ deps/rcheevos/src/rhash/cdreader.o \ deps/rcheevos/src/rhash/hash.o \ + deps/rcheevos/src/rhash/hash_disc.o \ + deps/rcheevos/src/rhash/hash_encrypted.o \ + deps/rcheevos/src/rhash/hash_rom.o \ + deps/rcheevos/src/rhash/hash_zip.o \ deps/rcheevos/src/rapi/rc_api_common.o \ deps/rcheevos/src/rapi/rc_api_info.o \ deps/rcheevos/src/rapi/rc_api_runtime.o \ diff --git a/cheevos/cheevos.c b/cheevos/cheevos.c index e6428fbd4f..6461db72af 100644 --- a/cheevos/cheevos.c +++ b/cheevos/cheevos.c @@ -96,6 +96,7 @@ static rcheevos_locals_t rcheevos_locals = 0, /* menuitem_count */ #endif true, /* hardcore_allowed */ + false,/* hardcore_requires_reload */ false,/* hardcore_being_enabled */ true /* core_supports */ }; @@ -953,6 +954,7 @@ void rcheevos_validate_config_settings(void) CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", buffer); _len = snprintf(buffer, sizeof(buffer), msg_hash_to_str(MSG_CHEEVOS_HARDCORE_PAUSED_SETTING_NOT_ALLOWED), key, val); + rcheevos_locals.hardcore_requires_reload = true; rcheevos_pause_hardcore(); runloop_msg_queue_push(buffer, _len, 0, 4 * 60, false, NULL, @@ -1587,6 +1589,7 @@ bool rcheevos_load(const void *data) rc_client_set_spectator_mode_enabled(rcheevos_locals.client, !rcheevos_is_player_active()); rc_client_set_read_memory_function(rcheevos_locals.client, rcheevos_client_read_memory_uninitialized); + rcheevos_locals.hardcore_requires_reload = false; rcheevos_validate_config_settings(); CHEEVOS_LOG(RCHEEVOS_TAG "Load started, hardcore %sactive\n", rcheevos_hardcore_active() ? "" : "not "); @@ -1652,7 +1655,7 @@ void rcheevos_change_disc(const char* new_disc_path, bool initial_disc) { if (rcheevos_locals.client) { - rc_client_begin_change_media(rcheevos_locals.client, new_disc_path, + rc_client_begin_identify_and_change_media(rcheevos_locals.client, new_disc_path, NULL, 0, rcheevos_client_change_media_callback, NULL); } } diff --git a/cheevos/cheevos_client.c b/cheevos/cheevos_client.c index 1b8c95fdb4..12c3d68187 100644 --- a/cheevos/cheevos_client.c +++ b/cheevos/cheevos_client.c @@ -420,7 +420,7 @@ static void rcheevos_client_download_task_callback(retro_task_t* task, free(callback_data); } -static bool rcheevos_client_download_badge(rc_client_download_queue_t* queue, +bool rcheevos_client_download_badge(rc_client_download_queue_t* queue, const char* url, const char* badge_name) { rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); @@ -472,11 +472,11 @@ void rcheevos_client_download_badge_from_url(const char* url, const char* badge_ static void rcheevos_client_fetch_next_badge(rc_client_download_queue_t* queue) { - rc_client_achievement_bucket_t* bucket; - rc_client_achievement_t* achievement; + const rc_client_achievement_bucket_t* bucket; + const rc_client_achievement_t* achievement; const char* next_badge; char badge_name[32]; - char url[256]; + const char* url = NULL; bool done = false; do @@ -515,9 +515,7 @@ static void rcheevos_client_fetch_next_badge(rc_client_download_queue_t* queue) if (queue->pass == 0) { /* first pass - get all unlocked badges */ - if (rc_client_achievement_get_image_url(achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url)) != RC_OK) - continue; - + url = achievement->badge_url; next_badge = achievement->badge_name; } else if (achievement->unlock_time) @@ -528,8 +526,7 @@ static void rcheevos_client_fetch_next_badge(rc_client_download_queue_t* queue) else { /* second pass - get locked badge */ - if (rc_client_achievement_get_image_url(achievement, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE, url, sizeof(url)) != RC_OK) - continue; + url = achievement->badge_locked_url; snprintf(badge_name, sizeof(badge_name), "%s_lock", achievement->badge_name); next_badge = badge_name; @@ -618,26 +615,5 @@ void rcheevos_client_download_achievement_badges(rc_client_t* client) #undef RCHEEVOS_CONCURRENT_BADGE_DOWNLOADS -void rcheevos_client_download_achievement_badge(const char* badge_name, bool locked) -{ - rc_api_fetch_image_request_t image_request; - rc_api_request_t request; - char locked_badge_name[32]; - - memset(&image_request, 0, sizeof(image_request)); - image_request.image_type = locked ? RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED : RC_IMAGE_TYPE_ACHIEVEMENT; - image_request.image_name = badge_name; - - if (locked) - { - snprintf(locked_badge_name, sizeof(locked_badge_name), "%s_lock", badge_name); - badge_name = locked_badge_name; - } - - if (rc_api_init_fetch_image_request(&request, &image_request) == RC_OK) - rcheevos_client_download_badge(NULL, request.url, badge_name); - - rc_api_destroy_request(&request); -} diff --git a/cheevos/cheevos_client.h b/cheevos/cheevos_client.h index 56dee4c20e..21f5b94a5f 100644 --- a/cheevos/cheevos_client.h +++ b/cheevos/cheevos_client.h @@ -23,7 +23,6 @@ RETRO_BEGIN_DECLS void rcheevos_client_download_placeholder_badge(void); void rcheevos_client_download_game_badge(const rc_client_game_t* game); void rcheevos_client_download_achievement_badges(rc_client_t* client); -void rcheevos_client_download_achievement_badge(const char* badge_name, bool locked); void rcheevos_client_download_badge_from_url(const char* url, const char* badge_name); void rcheevos_client_server_call(const rc_api_request_t* request, diff --git a/cheevos/cheevos_locals.h b/cheevos/cheevos_locals.h index 60fc83e54f..69caa7fecf 100644 --- a/cheevos/cheevos_locals.h +++ b/cheevos/cheevos_locals.h @@ -70,7 +70,7 @@ enum rcheevos_summary_notif typedef struct rcheevos_menuitem_t { - rc_client_achievement_t* achievement; + const rc_client_achievement_t* achievement; uintptr_t menu_badge_texture; uint32_t subset_id; uint8_t menu_badge_grayscale; @@ -98,6 +98,7 @@ typedef struct rcheevos_locals_t #endif bool hardcore_allowed; /* prevents enabling hardcore if illegal settings detected */ + bool hardcore_requires_reload; /* prevents enabling hardcore until the core is reloaded */ bool hardcore_being_enabled; /* allows callers to detect hardcore mode while it's being enabled */ bool core_supports; /* false if core explicitly disables achievements */ diff --git a/cheevos/cheevos_menu.c b/cheevos/cheevos_menu.c index d7ec053c2b..df8366d291 100644 --- a/cheevos/cheevos_menu.c +++ b/cheevos/cheevos_menu.c @@ -264,6 +264,14 @@ void rcheevos_menu_populate_hardcore_pause_submenu(void* data, bool cheevos_hard MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE, MENU_SETTING_ACTION_PAUSE_ACHIEVEMENTS, 0, 0, NULL); } + else if (rcheevos_locals->hardcore_requires_reload) + { + menu_entries_append(info->list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_RESUME_REQUIRES_RELOAD), + msg_hash_to_str(MENU_ENUM_LABEL_ACHIEVEMENT_RESUME_REQUIRES_RELOAD), + MENU_ENUM_LABEL_ACHIEVEMENT_RESUME_REQUIRES_RELOAD, + MENU_SETTING_ACTION_CLOSE, 0, 0, NULL); + } else { menu_entries_append(info->list, @@ -510,6 +518,47 @@ void rcheevos_menu_populate(void* data, bool cheevos_enable, #endif /* HAVE_MENU */ +static void rcheevos_client_download_achievement_badge(const char* badge_name, bool locked) +{ + /* have to find the achievement associated to badge_name, then fetch either badge_url + * or badge_locked_url based on the locked parameter */ + rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + rc_client_achievement_list_t* list = rc_client_create_achievement_list(rcheevos_locals->client, + RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + if (list) + { + const char* url = NULL; + uint32_t i, j; + for (i = 0; i < list->num_buckets && !url; i++) + { + for (j = 0; j < list->buckets[i].num_achievements; j++) + { + const rc_client_achievement_t* achievement = list->buckets[i].achievements[j]; + if (achievement && strcmp(achievement->badge_name, badge_name) == 0) + { + url = locked ? achievement->badge_locked_url : achievement->badge_url; + break; + } + } + } + + if (url) + { + char locked_badge_name[32]; + if (locked) + { + snprintf(locked_badge_name, sizeof(locked_badge_name), "%s_lock", badge_name); + badge_name = locked_badge_name; + } + + rcheevos_client_download_badge_from_url(url, badge_name); + } + + rc_client_destroy_achievement_list(list); + } +} + uintptr_t rcheevos_get_badge_texture(const char* badge, bool locked, bool download_if_missing) { size_t _len; @@ -542,21 +591,13 @@ uintptr_t rcheevos_get_badge_texture(const char* badge, bool locked, bool downlo { if (download_if_missing) { + const rcheevos_locals_t* rcheevos_locals = get_rcheevos_locals(); + if (badge[0] == 'i') { - /* rcheevos_client_download_game_badge expects a rc_client_game_t, not the badge name. - * call rc_api_init_fetch_image_request directly */ - rc_api_fetch_image_request_t image_request; - rc_api_request_t request; - int result; - - memset(&image_request, 0, sizeof(image_request)); - image_request.image_type = RC_IMAGE_TYPE_GAME; - image_request.image_name = &badge[1]; - result = rc_api_init_fetch_image_request(&request, &image_request); - - if (result == RC_OK) - rcheevos_client_download_badge_from_url(request.url, badge); + const rc_client_game_t* game = rc_client_get_game_info(rcheevos_locals->client); + if (game && strcmp(game->badge_name, &badge[1]) == 0) + rcheevos_client_download_badge_from_url(game->badge_url, badge); } else { diff --git a/deps/rcheevos/CHANGELOG.md b/deps/rcheevos/CHANGELOG.md index d770202b82..c06399e59e 100644 --- a/deps/rcheevos/CHANGELOG.md +++ b/deps/rcheevos/CHANGELOG.md @@ -1,3 +1,61 @@ +# v12.0.0 +* rc_client changes + * add RC_CLIENT_EVENT_SUBSET_COMPLETED event + * add 'avatar_url' to rc_client_user_t + * add 'badge_url' to rc_client_game_t + * add 'badge_url' and 'badge_locked_url' to rc_client_achievement_t + * add rc_client_set_hash_callbacks + * add rc_client_set_allow_background_memory_reads + * renamed rc_client_begin_change_media to rc_client_begin_identify_and_change_media + * renamed rc_client_begin_change_media_from_hash to rc_client_begin_change_media + * use slim read/write locks for rc_mutex on Windows Vista+ + * use critical sections for rc_mutex on older versions of Windows + * add translation layer in rc_client_external for cross-version support +* rhash changes + * rc_hash_init_verbose_message_callback and rc_hash_init_error_message_callback have been deprecated. + - set values in iterator.callbacks instead. + * rc_hash_init_custom_filereader and rc_hash_init_custom_cdreader have been deprecated. + - set values in iterator.callbacks instead. + * rc_hash_generate_from_file and rc_hash_generate_from_buffer have been deprecated. + - use rc_hash_initialize_iterator and rc_hash_generate instead. + * hash.c has been split into several smaller files which can be conditionally excluded from the build. + - hash_disc.c can be excluded by defining RC_HASH_NO_DISC + - hash_encrypted.c can be excluded by defining RC_HASH_NO_ENCRYPTED + - hash_rom.c can be excluded by defining RC_HASH_NO_ROM + - hash_zip.c can be excluded by defining RC_HASH_NO_ZIP + * add hash method for RC_CONSOLE_WII + * add hash method for Arduboy FX games (RC_CONSOLE_ARDUBOY) + * fix hash for PCE homebrew games that aren't multiples of 128KB +* rapi changes + * add _hosted variations of rc_api_init functions for interacting with custom hosts + - rc_api_set_host and rc_api_set_image_host have been deprecated + * add rc_api_fetch_game_sets + * add rc_api_fetch_followed_users + * add rc_api_fetch_hash_library + * add rc_api_fetch_all_user_progress + * add 'avatar_url' to login response, achievement_info response, and leaderboard_info responses + * add 'badge_url' to game_data response + * rurl has been removed (deprecated in v10.0.0) +* trigger parsing/processing code has been largely modified to improve processing performance + * AddAddress/AddSource chains can now be shared across triggers to avoid recalculating them multiple times + * triggers are preprocessed to allocate conditions as array instead of linked list. + - array is sorted such that similar conditions (pause/reset/hittarget/etc) can be processed together + without having to scan the linked list multiple times + - the linked list still exists for backwards compatibility and to maintain the original ordering + * each condition type has its own handler, eliminating many switches and branches when processing triggers + * memrefs are now allocated in arrays (eliminating the linked list pointer from each to reduce memory footprint) +* commas are now inserted into all numeric formats except SCORE. a new format type UNFORMATTED (@Unformatted) has + been added to display a number without commas. + - To forcibly exclude commas in a backwards-compatible way, use the @Unformatted macro and define Unformatted as + a VALUE formatter. Legacy clients will use the VALUE formatted and new clients will use the @Unformatted macro. +* add RC_CONSOLE_FAMICOM_DISK_SYSTEM (split off of RC_CONSOLE_NINTENDO) +* update RC_CONSOLE_SNES memory map to support SA-1 I-RAM and BW-RAM +* update RC_CONSOLE_SEGACD memory map to supprot WORD RAM +* update RC_CONSOLE_N64 memory map to place RDRAM at $80000000 and expansion pak RAM at $80400000 +* remove HAVE_LUA define and support (implementation was never finished) +* report error when RP contains address starting with `0x0x`. +* add .natvis files to improve debugging in Visual Studio + # v11.6.0 * backdate retried unlocks in rc_client * add memory map and hash method for RC_CONSOLE_ZX_SPECTRUM diff --git a/deps/rcheevos/README.md b/deps/rcheevos/README.md index b0b1a8c602..a838e5c195 100644 --- a/deps/rcheevos/README.md +++ b/deps/rcheevos/README.md @@ -6,23 +6,11 @@ Keep in mind that **rcheevos** does *not* provide HTTP network connections. Clie Not all structures defined by **rcheevos** can be created via the public API, but are exposed to allow interactions beyond just creation, destruction, and testing, such as the ones required by UI code that helps to create them. -## Lua - -RetroAchievements previously considered the use of the [Lua](https://www.lua.org) language to expand the syntax supported for creating achievements. - -To enable Lua support, you must compile with an additional compilation flag: `HAVE_LUA`, as neither the backend nor the UI for editing achievements are currently Lua-enabled. We do not foresee enabling it any time soon, but the code has not yet been completely eliminated as many of the low-level API fuctions have parameters for LUA data. - -> **rcheevos** does *not* create or maintain a Lua state, you have to create your own state and provide it to **rcheevos** to be used when Lua-coded achievements are found. Calls to **rcheevos** may allocate and/or free additional memory as part of the Lua runtime. - -Lua functions used in trigger operands receive two parameters: `peek`, which is used to read from the emulated system's memory, and `userdata`, which must be passed to `peek`. `peek`'s signature is the same as its C counterpart: - -```lua -function peek(address, num_bytes, userdata) -``` +**NOTE**: development occurs on the _develop_ branch, which is set as the default branch in GitHub so newly opened PRs will request to be merged into the _develop_ branch. When integrating **rcheevos** into your project, we recommend using the _master_ branch, which corresponds to the last official release, and minimizes the risk of encountering a bug that has been introduced since the last official release. ## API -An understanding about how achievements are developed may be useful, you can read more about it [here](http://docs.retroachievements.org/Developer-docs/). +An understanding about how achievements are developed may be useful, you can read more about it [here](https://docs.retroachievements.org/developer-docs/). Most of the exposed APIs are documented [here](https://github.com/RetroAchievements/rcheevos/wiki) @@ -41,13 +29,7 @@ Platforms supported by RetroAchievements are enumerated in `rc_consoles.h`. Note ## Runtime support -Provides a set of functions for managing an active game - initializing and processing achievements, leaderboards, and rich presence. When important things occur, events are raised for the caller via a callback. - -These are in `rc_runtime.h`. - -Note: `rc_runtime_t` still requires the client implement all of the logic that calls the APIs to retrieve the data and perform the unlocks. - -The `rc_client_t` functions wrap a `rc_runtime_t` and manage the API calls and other common functionality (like managing the user information, identifying/loading a game, and building the active/inactive achievements list for the UI). Please see [the wiki](https://github.com/RetroAchievements/rcheevos/wiki/rc_client-integration) for details on using the `rc_client_t` functions. +A set of functions for managing an active game is provided by the library. If you are considering adding achievement support to your emulator, you should look at the `rc_client_t` functions which will prepare the API calls and other implement other common functionality (like managing the user information, identifying/loading a game, and building the active/inactive achievements list for the UI). It has several callback functions which allow the client to implement dependent functionality (UI and HTTP calls). Please see [the wiki](https://github.com/RetroAchievements/rcheevos/wiki/rc_client-integration) for details on using the `rc_client_t` functions. ## Server Communication @@ -55,11 +37,9 @@ The `rc_client_t` functions wrap a `rc_runtime_t` and manage the API calls and o **rapi** does *not* make HTTP requests. -NOTE: **rapi** is a replacement for **rurl**. **rurl** has been deprecated. - NOTE: `rc_client` is the preferred way to have a client interact with the server. -These are in `rc_api_user.h`, `rc_api_runtime.h` and `rc_api_common.h`. +**rapi** headers are `rc_api_user.h`, `rc_api_runtime.h` and `rc_api_common.h`. The basic process of making an **rapi** call is to initialize a params object, call a function to convert it to a URL, send that to the server, then pass the response to a function to convert it into a response object, and handle the response values. @@ -76,6 +56,12 @@ Please see the [wiki](https://github.com/RetroAchievements/rcheevos/wiki) for de These are in `rc_hash.h`. ```c - int rc_hash_generate_from_buffer(char hash[33], int console_id, uint8_t* buffer, size_t buffer_size); - int rc_hash_generate_from_file(char hash[33], int console_id, const char* path); + void rc_hash_initialize_iterator(rc_hash_iterator_t* iterator, const char* path, const uint8_t* buffer, size_t buffer_size); + int rc_hash_generate(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator); + int rc_hash_iterate(char hash[33], rc_hash_iterator_t* iterator); + void rc_hash_destroy_iterator(rc_hash_iterator_t* iterator); ``` + +### Custom file handling + +**rhash** (and by extension **rc_client**) support custom handlers for opening/reading files. This allows the client to redirect file reads to support custom file formats (like ZIP or CHD). diff --git a/deps/rcheevos/include/rc_api_info.h b/deps/rcheevos/include/rc_api_info.h index 7d6cfa2bea..ee5121df05 100644 --- a/deps/rcheevos/include/rc_api_info.h +++ b/deps/rcheevos/include/rc_api_info.h @@ -33,6 +33,8 @@ rc_api_fetch_achievement_info_request_t; typedef struct rc_api_achievement_awarded_entry_t { /* The user associated to the entry */ const char* username; + /* A URL to the user's avatar image */ + const char* avatar_url; /* When the achievement was awarded */ time_t awarded; } @@ -62,6 +64,7 @@ typedef struct rc_api_fetch_achievement_info_response_t { rc_api_fetch_achievement_info_response_t; RC_EXPORT int RC_CCONV rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_achievement_info_request_hosted(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params, const rc_api_host_t* host); /* [deprecated] use rc_api_process_fetch_achievement_info_server_response instead */ RC_EXPORT int RC_CCONV rc_api_process_fetch_achievement_info_response(rc_api_fetch_achievement_info_response_t* response, const char* server_response); RC_EXPORT int RC_CCONV rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achievement_info_response_t* response, const rc_api_server_response_t* server_response); @@ -88,6 +91,8 @@ rc_api_fetch_leaderboard_info_request_t; typedef struct rc_api_lboard_info_entry_t { /* The user associated to the entry */ const char* username; + /* A URL to the user's avatar image */ + const char* avatar_url; /* The rank of the entry */ uint32_t rank; /* The index of the entry */ @@ -138,6 +143,7 @@ typedef struct rc_api_fetch_leaderboard_info_response_t { rc_api_fetch_leaderboard_info_response_t; RC_EXPORT int RC_CCONV rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_leaderboard_info_request_hosted(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params, const rc_api_host_t* host); /* [deprecated] use rc_api_process_fetch_leaderboard_info_server_response instead */ RC_EXPORT int RC_CCONV rc_api_process_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_info_response_t* response, const char* server_response); RC_EXPORT int RC_CCONV rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboard_info_response_t* response, const rc_api_server_response_t* server_response); @@ -178,6 +184,7 @@ typedef struct rc_api_fetch_games_list_response_t { rc_api_fetch_games_list_response_t; RC_EXPORT int RC_CCONV rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_games_list_request_hosted(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params, const rc_api_host_t* host); /* [deprecated] use rc_api_process_fetch_games_list_server_response instead */ RC_EXPORT int RC_CCONV rc_api_process_fetch_games_list_response(rc_api_fetch_games_list_response_t* response, const char* server_response); RC_EXPORT int RC_CCONV rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_response_t* response, const rc_api_server_response_t* server_response); @@ -222,9 +229,50 @@ typedef struct rc_api_fetch_game_titles_response_t { rc_api_fetch_game_titles_response_t; RC_EXPORT int RC_CCONV rc_api_init_fetch_game_titles_request(rc_api_request_t* request, const rc_api_fetch_game_titles_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_game_titles_request_hosted(rc_api_request_t* request, const rc_api_fetch_game_titles_request_t* api_params, const rc_api_host_t* host); RC_EXPORT int RC_CCONV rc_api_process_fetch_game_titles_server_response(rc_api_fetch_game_titles_response_t* response, const rc_api_server_response_t* server_response); RC_EXPORT void RC_CCONV rc_api_destroy_fetch_game_titles_response(rc_api_fetch_game_titles_response_t* response); +/* --- Fetch Game Hashes --- */ + +/** + * API parameters for a fetch games list request. + */ +typedef struct rc_api_fetch_hash_library_request_t { + /** + * The unique identifier of the console to query. + * Passing RC_CONSOLE_UNKNOWN will return hashes for all consoles. + */ + uint32_t console_id; +} rc_api_fetch_hash_library_request_t; + +/* A hash library entry */ +typedef struct rc_api_hash_library_entry_t { + /* The hash for the game */ + const char* hash; + /* The unique identifier of the game */ + uint32_t game_id; +} rc_api_hash_library_entry_t; + +/** + * Response data for a fetch hash library request. + */ +typedef struct rc_api_fetch_hash_library_response_t { + /* An array of entries, one per game */ + rc_api_hash_library_entry_t* entries; + /* The number of items in the entries array */ + uint32_t num_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_hash_library_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_hash_library_request(rc_api_request_t* request, const rc_api_fetch_hash_library_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_hash_library_request_hosted(rc_api_request_t* request, const rc_api_fetch_hash_library_request_t* api_params, const rc_api_host_t* host); +RC_EXPORT int RC_CCONV rc_api_process_fetch_hash_library_server_response(rc_api_fetch_hash_library_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_hash_library_response(rc_api_fetch_hash_library_response_t* response); + RC_END_C_DECLS #endif /* RC_API_INFO_H */ diff --git a/deps/rcheevos/include/rc_api_request.h b/deps/rcheevos/include/rc_api_request.h index dd72fb56de..ddccc7bc28 100644 --- a/deps/rcheevos/include/rc_api_request.h +++ b/deps/rcheevos/include/rc_api_request.h @@ -8,6 +8,17 @@ RC_BEGIN_C_DECLS +/** + * Information about the server being connected to. + */ +typedef struct rc_api_host_t { + /* The host name for the API calls (retroachievements.org) */ + const char* host; + /* The host name for media URLs (media.retroachievements.org) */ + const char* media_host; +} +rc_api_host_t; + /** * A constructed request to send to the retroachievements server. */ @@ -42,7 +53,9 @@ rc_api_response_t; RC_EXPORT void RC_CCONV rc_api_destroy_request(rc_api_request_t* request); +/* [deprecated] use rc_api_init_*_hosted instead */ RC_EXPORT void RC_CCONV rc_api_set_host(const char* hostname); +/* [deprecated] use rc_api_init_*_hosted instead */ RC_EXPORT void RC_CCONV rc_api_set_image_host(const char* hostname); typedef struct rc_api_server_response_t { diff --git a/deps/rcheevos/include/rc_api_runtime.h b/deps/rcheevos/include/rc_api_runtime.h index fef1b8769d..8a0de01b7f 100644 --- a/deps/rcheevos/include/rc_api_runtime.h +++ b/deps/rcheevos/include/rc_api_runtime.h @@ -28,6 +28,7 @@ rc_api_fetch_image_request_t; #define RC_IMAGE_TYPE_USER 4 RC_EXPORT int RC_CCONV rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_image_request_hosted(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params, const rc_api_host_t* host); /* --- Resolve Hash --- */ @@ -57,6 +58,8 @@ typedef struct rc_api_resolve_hash_response_t { rc_api_resolve_hash_response_t; RC_EXPORT int RC_CCONV rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_resolve_hash_request_hosted(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_resolve_hash_server_response instead */ RC_EXPORT int RC_CCONV rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response); RC_EXPORT int RC_CCONV rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response); RC_EXPORT void RC_CCONV rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response); @@ -73,6 +76,8 @@ typedef struct rc_api_fetch_game_data_request_t { const char* api_token; /* The unique identifier of the game */ uint32_t game_id; + /* The generated hash of the game to be identified (ignored if game_id is not 0) */ + const char* game_hash; } rc_api_fetch_game_data_request_t; @@ -105,7 +110,7 @@ typedef struct rc_api_achievement_definition_t { uint32_t category; /* The title of the achievement */ const char* title; - /* The dscription of the achievement */ + /* The description of the achievement */ const char* description; /* The definition of the achievement to be passed to rc_runtime_activate_achievement */ const char* definition; @@ -123,6 +128,10 @@ typedef struct rc_api_achievement_definition_t { float rarity; /* The approximate rarity of the achievement in hardcore (X% of users have earned the achievement in hardcore) */ float rarity_hardcore; + /* The URL for the achievement badge */ + const char* badge_url; + /* The URL for the locked achievement badge */ + const char* badge_locked_url; } rc_api_achievement_definition_t; @@ -146,6 +155,8 @@ typedef struct rc_api_fetch_game_data_response_t { const char* title; /* The image name for the game badge */ const char* image_name; + /* The URL for the game badge */ + const char* image_url; /* The rich presence script for the game to be passed to rc_runtime_activate_richpresence */ const char* rich_presence_script; @@ -165,10 +176,96 @@ typedef struct rc_api_fetch_game_data_response_t { rc_api_fetch_game_data_response_t; RC_EXPORT int RC_CCONV rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_game_data_request_hosted(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_fetch_game_data_server_response instead */ RC_EXPORT int RC_CCONV rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response); RC_EXPORT int RC_CCONV rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response); RC_EXPORT void RC_CCONV rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response); +/* --- Fetch Game Sets --- */ + +/** + * API parameters for a fetch game data request. + */ +typedef struct rc_api_fetch_game_sets_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the game */ + uint32_t game_id; + /* The generated hash of the game to be identified (ignored if game_id is not 0) */ + const char* game_hash; +} +rc_api_fetch_game_sets_request_t; + +#define RC_ACHIEVEMENT_SET_TYPE_CORE 0 +#define RC_ACHIEVEMENT_SET_TYPE_BONUS 1 +#define RC_ACHIEVEMENT_SET_TYPE_SPECIALTY 2 +#define RC_ACHIEVEMENT_SET_TYPE_EXCLUSIVE 3 + +/* A game subset definition */ +typedef struct rc_api_achievement_set_definition_t { + /* The unique identifier of the achievement set */ + uint32_t id; + /* The legacy game_id of the achievement set (used for editor API calls) */ + uint32_t game_id; + /* The title of the achievement set */ + const char* title; + /* The image name for the achievement set badge */ + const char* image_name; + /* The URL for the achievement set badge */ + const char* image_url; + + /* An array of achievements for the achievement set */ + rc_api_achievement_definition_t* achievements; + /* The number of items in the achievements array */ + uint32_t num_achievements; + + /* An array of leaderboards for the achievement set */ + rc_api_leaderboard_definition_t* leaderboards; + /* The number of items in the leaderboards array */ + uint32_t num_leaderboards; + + /* The type of the achievement set */ + uint8_t type; +} +rc_api_achievement_set_definition_t; + +/** + * Response data for a fetch game sets request. + */ +typedef struct rc_api_fetch_game_sets_response_t { + /* The unique identifier of the game */ + uint32_t id; + /* The console associated to the game */ + uint32_t console_id; + /* The title of the game */ + const char* title; + /* The image name for the game badge */ + const char* image_name; + /* The URL for the game badge */ + const char* image_url; + /* The rich presence script for the game to be passed to rc_runtime_activate_richpresence */ + const char* rich_presence_script; + /* The unique identifier of the game to use for session requests (startsession/ping/etc) */ + uint32_t session_game_id; + + /* An array of sets for the game */ + rc_api_achievement_set_definition_t* sets; + /* The number of items in the sets array */ + uint32_t num_sets; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_game_sets_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_game_sets_request(rc_api_request_t* request, const rc_api_fetch_game_sets_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_game_sets_request_hosted(rc_api_request_t* request, const rc_api_fetch_game_sets_request_t* api_params, const rc_api_host_t* host); +RC_EXPORT int RC_CCONV rc_api_process_fetch_game_sets_server_response(rc_api_fetch_game_sets_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_game_sets_response(rc_api_fetch_game_sets_response_t* response); + /* --- Ping --- */ /** @@ -200,6 +297,8 @@ typedef struct rc_api_ping_response_t { rc_api_ping_response_t; RC_EXPORT int RC_CCONV rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_ping_request_hosted(rc_api_request_t* request, const rc_api_ping_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_ping_server_response instead */ RC_EXPORT int RC_CCONV rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response); RC_EXPORT int RC_CCONV rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response); RC_EXPORT void RC_CCONV rc_api_destroy_ping_response(rc_api_ping_response_t* response); @@ -245,6 +344,8 @@ typedef struct rc_api_award_achievement_response_t { rc_api_award_achievement_response_t; RC_EXPORT int RC_CCONV rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_award_achievement_request_hosted(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_award_achievement_server_response instead */ RC_EXPORT int RC_CCONV rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response); RC_EXPORT int RC_CCONV rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response); RC_EXPORT void RC_CCONV rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response); @@ -305,6 +406,8 @@ typedef struct rc_api_submit_lboard_entry_response_t { rc_api_submit_lboard_entry_response_t; RC_EXPORT int RC_CCONV rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_submit_lboard_entry_request_hosted(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params, const rc_api_host_t* host); +/* [deprecated] use rc_api_process_submit_lboard_entry_server_response instead */ RC_EXPORT int RC_CCONV rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response); RC_EXPORT int RC_CCONV 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_EXPORT void RC_CCONV rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response); diff --git a/deps/rcheevos/include/rc_api_user.h b/deps/rcheevos/include/rc_api_user.h index f8e4dedde2..c977da9564 100644 --- a/deps/rcheevos/include/rc_api_user.h +++ b/deps/rcheevos/include/rc_api_user.h @@ -40,6 +40,8 @@ typedef struct rc_api_login_response_t { uint32_t num_unread_messages; /* The preferred name to display for the player */ const char* display_name; + /* A URL to the user's avatar image */ + const char* avatar_url; /* Common server-provided response information */ rc_api_response_t response; @@ -47,6 +49,7 @@ typedef struct rc_api_login_response_t { rc_api_login_response_t; RC_EXPORT int RC_CCONV rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_login_request_hosted(rc_api_request_t* request, const rc_api_login_request_t* api_params, const rc_api_host_t* host); /* [deprecated] use rc_api_process_login_server_response instead */ RC_EXPORT int RC_CCONV rc_api_process_login_response(rc_api_login_response_t* response, const char* server_response); RC_EXPORT int RC_CCONV rc_api_process_login_server_response(rc_api_login_response_t* response, const rc_api_server_response_t* server_response); @@ -105,6 +108,7 @@ typedef struct rc_api_start_session_response_t { rc_api_start_session_response_t; RC_EXPORT int RC_CCONV rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_start_session_request_hosted(rc_api_request_t* request, const rc_api_start_session_request_t* api_params, const rc_api_host_t* host); /* [deprecated] use rc_api_process_start_session_server_response instead */ RC_EXPORT int RC_CCONV rc_api_process_start_session_response(rc_api_start_session_response_t* response, const char* server_response); RC_EXPORT int RC_CCONV rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response); @@ -142,11 +146,117 @@ typedef struct rc_api_fetch_user_unlocks_response_t { rc_api_fetch_user_unlocks_response_t; RC_EXPORT int RC_CCONV rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_user_unlocks_request_hosted(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params, const rc_api_host_t* host); /* [deprecated] use rc_api_process_fetch_user_unlocks_server_response instead */ RC_EXPORT int RC_CCONV rc_api_process_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response, const char* server_response); RC_EXPORT int RC_CCONV rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_response_t* response, const rc_api_server_response_t* server_response); RC_EXPORT void RC_CCONV rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response); +/* --- Fetch Followed Users --- */ + +/** + * API parameters for a fetch followed users request. + */ +typedef struct rc_api_fetch_followed_users_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; +} +rc_api_fetch_followed_users_request_t; + +typedef struct rc_api_followed_user_activity_t { + /* The record associated to the activity */ + const char* context; + /* The image of the record associated to the activity */ + const char* context_image_url; + /* The description of the activity */ + const char* description; + /* The time of the activity */ + time_t when; + /* The unique identifier of the record associated to the activity */ + uint32_t context_id; +} +rc_api_followed_user_activity_t; + +/** + * Response data for a followed user. + */ +typedef struct rc_api_followed_user_t { + /* The preferred name to display for the player */ + const char* display_name; + /* A URL to the user's avatar image */ + const char* avatar_url; + /* The player's last registered activity */ + rc_api_followed_user_activity_t recent_activity; + /* The current score of the player */ + uint32_t score; +} +rc_api_followed_user_t; + +/** + * Response data for a fetch followed users request. + */ +typedef struct rc_api_fetch_followed_users_response_t { + /* An array of followed user information */ + rc_api_followed_user_t* users; + /* The number of items in the users array */ + uint32_t num_users; + + /* Common server-provided response information */ + rc_api_response_t response; +} +rc_api_fetch_followed_users_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_followed_users_request(rc_api_request_t* request, const rc_api_fetch_followed_users_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_followed_users_request_hosted(rc_api_request_t* request, const rc_api_fetch_followed_users_request_t* api_params, const rc_api_host_t* host); +RC_EXPORT int RC_CCONV rc_api_process_fetch_followed_users_server_response(rc_api_fetch_followed_users_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_followed_users_response(rc_api_fetch_followed_users_response_t* response); + +/* --- Fetch All Progress --- */ + +/** + * API parameters for a fetch all user progress request. + */ +typedef struct rc_api_fetch_all_user_progress_request_t { + /* The username of the player */ + const char* username; + /* The API token from the login request */ + const char* api_token; + /* The unique identifier of the console to query */ + uint32_t console_id; +} rc_api_fetch_all_user_progress_request_t; + +/* An all-user-progress entry */ +typedef struct rc_api_all_user_progress_entry_t { + /* The unique identifier of the game */ + uint32_t game_id; + /* The total number of achievements for this game */ + uint32_t num_achievements; + /* The total number of unlocked achievements for this game in softcore mode */ + uint32_t num_unlocked_achievements; + /* The total number of unlocked achievements for this game in hardcore mode */ + uint32_t num_unlocked_achievements_hardcore; +} rc_api_all_user_progress_entry_t; + +/** + * Response data for a fetch all user progress request. + */ +typedef struct rc_api_fetch_all_user_progress_response_t { + /* An array of entries, one per game */ + rc_api_all_user_progress_entry_t* entries; + /* The number of items in the entries array */ + uint32_t num_entries; + + /* Common server-provided response information */ + rc_api_response_t response; +} rc_api_fetch_all_user_progress_response_t; + +RC_EXPORT int RC_CCONV rc_api_init_fetch_all_user_progress_request(rc_api_request_t* request, const rc_api_fetch_all_user_progress_request_t* api_params); +RC_EXPORT int RC_CCONV rc_api_init_fetch_all_user_progress_request_hosted(rc_api_request_t* request, const rc_api_fetch_all_user_progress_request_t* api_params, const rc_api_host_t* host); +RC_EXPORT int RC_CCONV rc_api_process_fetch_all_user_progress_server_response(rc_api_fetch_all_user_progress_response_t* response, const rc_api_server_response_t* server_response); +RC_EXPORT void RC_CCONV rc_api_destroy_fetch_all_user_progress_response(rc_api_fetch_all_user_progress_response_t* response); + RC_END_C_DECLS #endif /* RC_API_H */ diff --git a/deps/rcheevos/include/rc_client.h b/deps/rcheevos/include/rc_client.h index 26db52932d..89fb0a103e 100644 --- a/deps/rcheevos/include/rc_client.h +++ b/deps/rcheevos/include/rc_client.h @@ -121,7 +121,7 @@ RC_EXPORT void* RC_CCONV rc_client_get_userdata(const rc_client_t* client); /** * Sets the name of the server to use. */ -RC_EXPORT void RC_CCONV rc_client_set_host(const rc_client_t* client, const char* hostname); +RC_EXPORT void RC_CCONV rc_client_set_host(rc_client_t* client, const char* hostname); typedef uint64_t rc_clock_t; typedef rc_clock_t (RC_CCONV *rc_get_time_millisecs_func_t)(const rc_client_t* client); @@ -188,6 +188,8 @@ typedef struct rc_client_user_t { uint32_t score; uint32_t score_softcore; uint32_t num_unread_messages; + /* minimum version: 12.0 */ + const char* avatar_url; } rc_client_user_t; /** @@ -217,6 +219,39 @@ typedef struct rc_client_user_game_summary_t { */ RC_EXPORT void RC_CCONV rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary); +typedef struct rc_client_all_user_progress_entry_t { + uint32_t game_id; + uint32_t num_achievements; + uint32_t num_unlocked_achievements; + uint32_t num_unlocked_achievements_hardcore; +} rc_client_all_user_progress_entry_t; + +typedef struct rc_client_all_user_progress_t { + rc_client_all_user_progress_entry_t* entries; + uint32_t num_entries; +} rc_client_all_user_progress_t; + +/** + * Callback that is fired when an all progress query completes. list may be null if the query failed. + */ +typedef void(RC_CCONV* rc_client_fetch_all_user_progress_callback_t)(int result, const char* error_message, + rc_client_all_user_progress_t* list, + rc_client_t* client, void* callback_userdata); + +/** + * Starts an asynchronous request for all progress for the given console. + * This query returns the total number of achievements for all games tracked by this console, as well as + * the user's achievement unlock count for both softcore and hardcore modes. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV +rc_client_begin_fetch_all_user_progress(rc_client_t* client, uint32_t console_id, + rc_client_fetch_all_user_progress_callback_t callback, void* callback_userdata); + +/** + * Destroys a previously-allocated result from the rc_client_begin_fetch_all_progress_list() callback. + */ +RC_EXPORT void RC_CCONV rc_client_destroy_all_user_progress(rc_client_all_user_progress_t* list); + /*****************************************************************************\ | Game | \*****************************************************************************/ @@ -229,6 +264,12 @@ RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_identify_and_load_g uint32_t console_id, const char* file_path, const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); + +struct rc_hash_callbacks; +/** + * Provide callback functions for interacting with the file system and processing disc-based files when generating hashes. + */ +RC_EXPORT void rc_client_set_hash_callbacks(rc_client_t* client, const struct rc_hash_callbacks* callbacks); #endif /** @@ -243,9 +284,9 @@ RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_load_game(rc_client RC_EXPORT int RC_CCONV rc_client_get_load_game_state(const rc_client_t* client); enum { RC_CLIENT_LOAD_GAME_STATE_NONE, - RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME, RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN, - RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA, + RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME, + RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA, /* [deprecated] - game data is now returned by identify call */ RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION, RC_CLIENT_LOAD_GAME_STATE_DONE, RC_CLIENT_LOAD_GAME_STATE_ABORTED @@ -267,6 +308,8 @@ typedef struct rc_client_game_t { const char* title; const char* hash; const char* badge_name; + /* minimum version: 12.0 */ + const char* badge_url; } rc_client_game_t; /** @@ -285,15 +328,17 @@ RC_EXPORT int RC_CCONV rc_client_game_get_image_url(const rc_client_game_t* game /** * Changes the active disc in a multi-disc game. */ -RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_change_media(rc_client_t* client, const char* file_path, +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_identify_and_change_media(rc_client_t* client, const char* file_path, const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); #endif /** * Changes the active disc in a multi-disc game. */ -RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_change_media_from_hash(rc_client_t* client, const char* hash, +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_change_media(rc_client_t* client, const char* hash, rc_client_callback_t callback, void* callback_userdata); +/* this function was renamed in rcheevos 12.0 */ +#define rc_client_begin_change_media_from_hash rc_client_begin_change_media /*****************************************************************************\ | Subsets | @@ -306,10 +351,47 @@ typedef struct rc_client_subset_t { uint32_t num_achievements; uint32_t num_leaderboards; + + /* minimum version: 12.0 */ + const char* badge_url; } rc_client_subset_t; RC_EXPORT const rc_client_subset_t* RC_CCONV rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id); +/*****************************************************************************\ +| Fetch Game Hashes | +\*****************************************************************************/ + +typedef struct rc_client_hash_library_entry_t { + char hash[33]; + uint32_t game_id; +} rc_client_hash_library_entry_t; + +typedef struct rc_client_hash_library_t { + rc_client_hash_library_entry_t* entries; + uint32_t num_entries; +} rc_client_hash_library_t; + +/** + * Callback that is fired when a hash library request completes. list may be null if the query failed. + */ +typedef void(RC_CCONV* rc_client_fetch_hash_library_callback_t)(int result, const char* error_message, + rc_client_hash_library_t* list, rc_client_t* client, + void* callback_userdata); + +/** + * Starts an asynchronous request for all hashes for the given console. + * This request returns a mapping from hashes to the game's unique identifier. A single game may have multiple + * hashes in the case of multi-disc games, or variants that are still compatible with the same achievement set. + */ +RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_fetch_hash_library( + rc_client_t* client, uint32_t console_id, rc_client_fetch_hash_library_callback_t callback, void* callback_userdata); + +/** + * Destroys a previously-allocated result from the rc_client_destroy_hash_library() callback. + */ +RC_EXPORT void RC_CCONV rc_client_destroy_hash_library(rc_client_hash_library_t* list); + /*****************************************************************************\ | Achievements | \*****************************************************************************/ @@ -373,6 +455,9 @@ typedef struct rc_client_achievement_t { float rarity; float rarity_hardcore; uint8_t type; + /* minimum version: 12.0 */ + const char* badge_url; + const char* badge_locked_url; } rc_client_achievement_t; /** @@ -387,7 +472,7 @@ RC_EXPORT const rc_client_achievement_t* RC_CCONV rc_client_get_achievement_info RC_EXPORT int RC_CCONV 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; + const rc_client_achievement_t** achievements; uint32_t num_achievements; const char* label; @@ -396,7 +481,7 @@ typedef struct rc_client_achievement_bucket_t { } rc_client_achievement_bucket_t; typedef struct rc_client_achievement_list_t { - rc_client_achievement_bucket_t* buckets; + const rc_client_achievement_bucket_t* buckets; uint32_t num_buckets; } rc_client_achievement_list_t; @@ -463,7 +548,7 @@ typedef struct rc_client_leaderboard_tracker_t { } rc_client_leaderboard_tracker_t; typedef struct rc_client_leaderboard_bucket_t { - rc_client_leaderboard_t** leaderboards; + const rc_client_leaderboard_t** leaderboards; uint32_t num_leaderboards; const char* label; @@ -472,7 +557,7 @@ typedef struct rc_client_leaderboard_bucket_t { } rc_client_leaderboard_bucket_t; typedef struct rc_client_leaderboard_list_t { - rc_client_leaderboard_bucket_t* buckets; + const rc_client_leaderboard_bucket_t* buckets; uint32_t num_buckets; } rc_client_leaderboard_list_t; @@ -619,7 +704,8 @@ enum { RC_CLIENT_EVENT_GAME_COMPLETED = 15, /* all achievements for the game have been earned */ RC_CLIENT_EVENT_SERVER_ERROR = 16, /* an API response returned a [server_error] and will not be retried */ RC_CLIENT_EVENT_DISCONNECTED = 17, /* an unlock request could not be completed and is pending */ - RC_CLIENT_EVENT_RECONNECTED = 18 /* all pending unlocks have been completed */ + RC_CLIENT_EVENT_RECONNECTED = 18, /* all pending unlocks have been completed */ + RC_CLIENT_EVENT_SUBSET_COMPLETED = 19 /* all achievements for the subset have been earned */ }; typedef struct rc_client_server_error_t { @@ -637,6 +723,7 @@ typedef struct rc_client_event_t { rc_client_leaderboard_tracker_t* leaderboard_tracker; rc_client_leaderboard_scoreboard_t* leaderboard_scoreboard; rc_client_server_error_t* server_error; + rc_client_subset_t* subset; } rc_client_event_t; @@ -655,6 +742,11 @@ RC_EXPORT void RC_CCONV rc_client_set_event_handler(rc_client_t* client, rc_clie */ RC_EXPORT void RC_CCONV rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler); +/** + * Specifies whether rc_client is allowed to read memory outside of rc_client_do_frame/rc_client_idle. + */ +RC_EXPORT void RC_CCONV rc_client_set_allow_background_memory_reads(rc_client_t* client, int allowed); + /** * Determines if there are any active achievements/leaderboards/rich presence that need processing. */ diff --git a/deps/rcheevos/include/rc_consoles.h b/deps/rcheevos/include/rc_consoles.h index 269879202b..5f788855b1 100644 --- a/deps/rcheevos/include/rc_consoles.h +++ b/deps/rcheevos/include/rc_consoles.h @@ -93,6 +93,7 @@ enum { RC_CONSOLE_NINTENDO_DSI = 78, RC_CONSOLE_TI83 = 79, RC_CONSOLE_UZEBOX = 80, + RC_CONSOLE_FAMICOM_DISK_SYSTEM = 81, RC_CONSOLE_HUBS = 100, RC_CONSOLE_EVENTS = 101, diff --git a/deps/rcheevos/include/rc_error.h b/deps/rcheevos/include/rc_error.h index 171bbf8e94..5d253442da 100644 --- a/deps/rcheevos/include/rc_error.h +++ b/deps/rcheevos/include/rc_error.h @@ -11,7 +11,7 @@ RC_BEGIN_C_DECLS enum { RC_OK = 0, - RC_INVALID_LUA_OPERAND = -1, + RC_INVALID_FUNC_OPERAND = -1, RC_INVALID_MEMORY_OPERAND = -2, RC_INVALID_CONST_OPERAND = -3, RC_INVALID_FP_OPERAND = -4, @@ -48,7 +48,8 @@ enum { RC_EXPIRED_TOKEN = -35, RC_INSUFFICIENT_BUFFER = -36, RC_INVALID_VARIABLE_NAME = -37, - RC_UNKNOWN_VARIABLE_NAME = -38 + RC_UNKNOWN_VARIABLE_NAME = -38, + RC_NOT_FOUND = -39 }; RC_EXPORT const char* RC_CCONV rc_error_str(int ret); diff --git a/deps/rcheevos/include/rc_hash.h b/deps/rcheevos/include/rc_hash.h index 3a71bcebf8..18661e4b12 100644 --- a/deps/rcheevos/include/rc_hash.h +++ b/deps/rcheevos/include/rc_hash.h @@ -9,54 +9,19 @@ RC_BEGIN_C_DECLS - /* ===================================================== */ - - /* generates a hash from a block of memory. - * returns non-zero on success, or zero on failure. - */ - RC_EXPORT int RC_CCONV rc_hash_generate_from_buffer(char hash[33], uint32_t console_id, const uint8_t* buffer, size_t buffer_size); - - /* generates a hash from a file. - * returns non-zero on success, or zero on failure. - */ - RC_EXPORT int RC_CCONV rc_hash_generate_from_file(char hash[33], uint32_t console_id, const char* path); + struct rc_hash_iterator; /* ===================================================== */ - /* data for rc_hash_iterate - */ - typedef struct rc_hash_iterator - { - 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) - */ - RC_EXPORT void RC_CCONV 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 - */ - RC_EXPORT void RC_CCONV rc_hash_destroy_iterator(struct rc_hash_iterator* iterator); - - /* generates the next hash for the data in the rc_hash_iterator. - * returns non-zero if a hash was generated, or zero if no more hashes can be generated for the data. - */ - RC_EXPORT int RC_CCONV rc_hash_iterate(char hash[33], struct rc_hash_iterator* iterator); - - /* ===================================================== */ + typedef void (RC_CCONV *rc_hash_message_callback_deprecated)(const char*); /* specifies a function to call when an error occurs to display the error message */ - typedef void (RC_CCONV *rc_hash_message_callback)(const char*); - RC_EXPORT void RC_CCONV rc_hash_init_error_message_callback(rc_hash_message_callback callback); + /* [deprecated] set callbacks in rc_hash_iterator_t */ + RC_EXPORT void RC_CCONV rc_hash_init_error_message_callback(rc_hash_message_callback_deprecated callback); /* specifies a function to call for verbose logging */ - RC_EXPORT void rc_hash_init_verbose_message_callback(rc_hash_message_callback callback); + /* [deprecated] set callbacks in rc_hash_iterator_t */ + RC_EXPORT void rc_hash_init_verbose_message_callback(rc_hash_message_callback_deprecated callback); /* ===================================================== */ @@ -77,19 +42,22 @@ RC_BEGIN_C_DECLS /* closes the file */ typedef void (RC_CCONV *rc_hash_filereader_close_file_handler)(void* file_handle); - struct rc_hash_filereader + typedef struct rc_hash_filereader { rc_hash_filereader_open_file_handler open; rc_hash_filereader_seek_handler seek; rc_hash_filereader_tell_handler tell; rc_hash_filereader_read_handler read; rc_hash_filereader_close_file_handler close; - }; + } rc_hash_filereader_t; + /* [deprecated] set callbacks in rc_hash_iterator_t */ RC_EXPORT void RC_CCONV rc_hash_init_custom_filereader(struct rc_hash_filereader* reader); /* ===================================================== */ +#ifndef RC_HASH_NO_DISC + #define RC_HASH_CDTRACK_FIRST_DATA ((uint32_t)-1) /* the first data track (skip audio tracks) */ #define RC_HASH_CDTRACK_LAST ((uint32_t)-2) /* the last data/audio track */ #define RC_HASH_CDTRACK_LARGEST ((uint32_t)-3) /* the largest data/audio track */ @@ -99,6 +67,7 @@ RC_BEGIN_C_DECLS * returns a handle to be passed to the other functions, or NULL if the track could not be opened. */ typedef void* (RC_CCONV *rc_hash_cdreader_open_track_handler)(const char* path, uint32_t track); + typedef void* (RC_CCONV* rc_hash_cdreader_open_track_iterator_handler)(const char* path, uint32_t track, const struct rc_hash_iterator* iterator); /* attempts to read the specified number of bytes from the file starting at the specified absolute sector. * returns the number of bytes actually read. @@ -111,24 +80,32 @@ RC_BEGIN_C_DECLS /* gets the absolute sector index for the first sector of a track */ typedef uint32_t(RC_CCONV *rc_hash_cdreader_first_track_sector_handler)(void* track_handle); - struct rc_hash_cdreader + typedef struct rc_hash_cdreader { rc_hash_cdreader_open_track_handler open_track; rc_hash_cdreader_read_sector_handler read_sector; rc_hash_cdreader_close_track_handler close_track; rc_hash_cdreader_first_track_sector_handler first_track_sector; - }; + rc_hash_cdreader_open_track_iterator_handler open_track_iterator; + } rc_hash_cdreader_t; RC_EXPORT void RC_CCONV rc_hash_get_default_cdreader(struct rc_hash_cdreader* cdreader); + /* [deprecated] don't set callbacks in rc_hash_iterator_t */ RC_EXPORT void RC_CCONV rc_hash_init_default_cdreader(void); + /* [deprecated] set callbacks in rc_hash_iterator_t */ RC_EXPORT void RC_CCONV rc_hash_init_custom_cdreader(struct rc_hash_cdreader* reader); +#endif /* RC_HASH_NO_DISC */ + +#ifndef RC_HASH_NO_ENCRYPTED + /* specifies a function called to obtain a 3DS CIA decryption normal key. * this key would be derived from slot0x3DKeyX and the common key specified by the passed index. * the normal key should be written in big endian format * returns non-zero on success, or zero on failure. */ typedef int (RC_CCONV *rc_hash_3ds_get_cia_normal_key_func)(uint8_t common_key_index, uint8_t out_normal_key[16]); + /* [deprecated] set callbacks in rc_hash_iterator_t */ RC_EXPORT void RC_CCONV rc_hash_init_3ds_get_cia_normal_key_func(rc_hash_3ds_get_cia_normal_key_func func); /* specifies a function called to obtain 3DS NCCH decryption normal keys. @@ -143,9 +120,79 @@ RC_BEGIN_C_DECLS */ typedef int (RC_CCONV *rc_hash_3ds_get_ncch_normal_keys_func)(uint8_t primary_key_y[16], uint8_t secondary_key_x_slot, uint8_t* optional_program_id, uint8_t out_primary_key[16], uint8_t out_secondary_key[16]); + /* [deprecated] set callbacks in rc_hash_iterator_t */ RC_EXPORT void RC_CCONV rc_hash_init_3ds_get_ncch_normal_keys_func(rc_hash_3ds_get_ncch_normal_keys_func func); - /* ===================================================== */ +#endif /* RC_HASH_NO_ENCRYPTED */ + +/* ===================================================== */ + +typedef void (RC_CCONV* rc_hash_message_callback_func)(const char*, const struct rc_hash_iterator* iterator); + +typedef struct rc_hash_callbacks { + rc_hash_message_callback_func verbose_message; + rc_hash_message_callback_func error_message; + + rc_hash_filereader_t filereader; +#ifndef RC_HASH_NO_DISC + rc_hash_cdreader_t cdreader; +#endif + +#ifndef RC_HASH_NO_ENCRYPTED + struct rc_hash_encryption_callbacks { + rc_hash_3ds_get_cia_normal_key_func get_3ds_cia_normal_key; + rc_hash_3ds_get_ncch_normal_keys_func get_3ds_ncch_normal_keys; + } encryption; +#endif +} rc_hash_callbacks_t; + +/* data for rc_hash_iterate + */ +typedef struct rc_hash_iterator { + const uint8_t* buffer; + size_t buffer_size; + uint8_t consoles[12]; + int index; + const char* path; + + rc_hash_callbacks_t callbacks; +} 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) + */ +RC_EXPORT void RC_CCONV rc_hash_initialize_iterator(rc_hash_iterator_t* iterator, const char* path, const uint8_t* buffer, size_t buffer_size); + +/* releases resources associated to a rc_hash_iterator + */ +RC_EXPORT void RC_CCONV rc_hash_destroy_iterator(rc_hash_iterator_t* iterator); + +/* generates the next hash for the data in the rc_hash_iterator. + * returns non-zero if a hash was generated, or zero if no more hashes can be generated for the data. + */ +RC_EXPORT int RC_CCONV rc_hash_iterate(char hash[33], rc_hash_iterator_t* iterator); + +/* generates a hash for the data in the rc_hash_iterator. + * returns non-zero if a hash was generated. + */ +RC_EXPORT int RC_CCONV rc_hash_generate(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator); + +/* ===================================================== */ + +/* generates a hash from a block of memory. + * returns non-zero on success, or zero on failure. + */ +/* [deprecated] use rc_hash_generate instead */ +RC_EXPORT int RC_CCONV rc_hash_generate_from_buffer(char hash[33], uint32_t console_id, const uint8_t* buffer, size_t buffer_size); + +/* generates a hash from a file. + * returns non-zero on success, or zero on failure. + */ +/* [deprecated] use rc_hash_generate instead */ +RC_EXPORT int RC_CCONV rc_hash_generate_from_file(char hash[33], uint32_t console_id, const char* path); + +/* ===================================================== */ RC_END_C_DECLS diff --git a/deps/rcheevos/include/rc_runtime.h b/deps/rcheevos/include/rc_runtime.h index d778fde5ce..88b12dea27 100644 --- a/deps/rcheevos/include/rc_runtime.h +++ b/deps/rcheevos/include/rc_runtime.h @@ -14,8 +14,6 @@ RC_BEGIN_C_DECLS #ifndef RC_RUNTIME_TYPES_H /* prevents pedantic redefinition error */ -typedef struct lua_State lua_State; - typedef struct rc_trigger_t rc_trigger_t; typedef struct rc_lboard_t rc_lboard_t; typedef struct rc_richpresence_t rc_richpresence_t; @@ -46,7 +44,6 @@ typedef struct rc_runtime_trigger_t { rc_memref_t* invalid_memref; uint8_t md5[16]; int32_t serialized_size; - uint8_t owns_memrefs; } rc_runtime_trigger_t; @@ -58,16 +55,13 @@ typedef struct rc_runtime_lboard_t { rc_memref_t* invalid_memref; uint8_t md5[16]; uint32_t serialized_size; - uint8_t owns_memrefs; } rc_runtime_lboard_t; typedef struct rc_runtime_richpresence_t { rc_richpresence_t* richpresence; void* buffer; - struct rc_runtime_richpresence_t* previous; uint8_t md5[16]; - uint8_t owns_memrefs; } rc_runtime_richpresence_t; @@ -82,11 +76,7 @@ typedef struct rc_runtime_t { rc_runtime_richpresence_t* richpresence; - rc_memref_t* memrefs; - rc_memref_t** next_memref; - - rc_value_t* variables; - rc_value_t** next_variable; + struct rc_memrefs_t* memrefs; uint8_t owns_self; } @@ -96,20 +86,20 @@ RC_EXPORT rc_runtime_t* RC_CCONV rc_runtime_alloc(void); RC_EXPORT void RC_CCONV rc_runtime_init(rc_runtime_t* runtime); RC_EXPORT void RC_CCONV rc_runtime_destroy(rc_runtime_t* runtime); -RC_EXPORT int RC_CCONV rc_runtime_activate_achievement(rc_runtime_t* runtime, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx); +RC_EXPORT int RC_CCONV rc_runtime_activate_achievement(rc_runtime_t* runtime, uint32_t id, const char* memaddr, void* unused_L, int unused_funcs_idx); RC_EXPORT void RC_CCONV rc_runtime_deactivate_achievement(rc_runtime_t* runtime, uint32_t id); RC_EXPORT rc_trigger_t* RC_CCONV rc_runtime_get_achievement(const rc_runtime_t* runtime, uint32_t id); RC_EXPORT int RC_CCONV rc_runtime_get_achievement_measured(const rc_runtime_t* runtime, uint32_t id, unsigned* measured_value, unsigned* measured_target); RC_EXPORT int RC_CCONV rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, uint32_t id, char *buffer, size_t buffer_size); -RC_EXPORT int RC_CCONV rc_runtime_activate_lboard(rc_runtime_t* runtime, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx); +RC_EXPORT int RC_CCONV rc_runtime_activate_lboard(rc_runtime_t* runtime, uint32_t id, const char* memaddr, void* unused_L, int unused_funcs_idx); RC_EXPORT void RC_CCONV rc_runtime_deactivate_lboard(rc_runtime_t* runtime, uint32_t id); RC_EXPORT rc_lboard_t* RC_CCONV rc_runtime_get_lboard(const rc_runtime_t* runtime, uint32_t id); RC_EXPORT int RC_CCONV rc_runtime_format_lboard_value(char* buffer, int size, int32_t value, int format); -RC_EXPORT int RC_CCONV rc_runtime_activate_richpresence(rc_runtime_t* runtime, const char* script, lua_State* L, int funcs_idx); -RC_EXPORT int RC_CCONV rc_runtime_get_richpresence(const rc_runtime_t* runtime, char* buffer, size_t buffersize, rc_runtime_peek_t peek, void* peek_ud, lua_State* L); +RC_EXPORT int RC_CCONV rc_runtime_activate_richpresence(rc_runtime_t* runtime, const char* script, void* unused_L, int unused_funcs_idx); +RC_EXPORT int RC_CCONV rc_runtime_get_richpresence(const rc_runtime_t* runtime, char* buffer, size_t buffersize, rc_runtime_peek_t peek, void* peek_ud, void* unused_L); enum { RC_RUNTIME_EVENT_ACHIEVEMENT_ACTIVATED, /* from WAITING, PAUSED, or PRIMED to ACTIVE */ @@ -136,22 +126,22 @@ rc_runtime_event_t; typedef void (RC_CCONV *rc_runtime_event_handler_t)(const rc_runtime_event_t* runtime_event); -RC_EXPORT void RC_CCONV rc_runtime_do_frame(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, lua_State* L); +RC_EXPORT void RC_CCONV rc_runtime_do_frame(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, void* unused_L); RC_EXPORT void RC_CCONV rc_runtime_reset(rc_runtime_t* runtime); typedef int (RC_CCONV *rc_runtime_validate_address_t)(uint32_t address); RC_EXPORT void RC_CCONV rc_runtime_validate_addresses(rc_runtime_t* runtime, rc_runtime_event_handler_t event_handler, rc_runtime_validate_address_t validate_handler); RC_EXPORT void RC_CCONV rc_runtime_invalidate_address(rc_runtime_t* runtime, uint32_t address); -RC_EXPORT uint32_t RC_CCONV rc_runtime_progress_size(const rc_runtime_t* runtime, lua_State* L); +RC_EXPORT uint32_t RC_CCONV rc_runtime_progress_size(const rc_runtime_t* runtime, void* unused_L); /* [deprecated] use rc_runtime_serialize_progress_sized instead */ -RC_EXPORT int RC_CCONV rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, lua_State* L); -RC_EXPORT int RC_CCONV rc_runtime_serialize_progress_sized(uint8_t* buffer, uint32_t buffer_size, const rc_runtime_t* runtime, lua_State* L); +RC_EXPORT int RC_CCONV rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, void* unused_L); +RC_EXPORT int RC_CCONV rc_runtime_serialize_progress_sized(uint8_t* buffer, uint32_t buffer_size, const rc_runtime_t* runtime, void* unused_L); /* [deprecated] use rc_runtime_deserialize_progress_sized instead */ -RC_EXPORT int RC_CCONV rc_runtime_deserialize_progress(rc_runtime_t* runtime, const uint8_t* serialized, lua_State* L); -RC_EXPORT int RC_CCONV rc_runtime_deserialize_progress_sized(rc_runtime_t* runtime, const uint8_t* serialized, uint32_t serialized_size, lua_State* L); +RC_EXPORT int RC_CCONV rc_runtime_deserialize_progress(rc_runtime_t* runtime, const uint8_t* serialized, void* unused_L); +RC_EXPORT int RC_CCONV rc_runtime_deserialize_progress_sized(rc_runtime_t* runtime, const uint8_t* serialized, uint32_t serialized_size, void* unused_L); RC_END_C_DECLS diff --git a/deps/rcheevos/include/rc_runtime_types.h b/deps/rcheevos/include/rc_runtime_types.h index 4bf1b13bfc..81ede5a9bb 100644 --- a/deps/rcheevos/include/rc_runtime_types.h +++ b/deps/rcheevos/include/rc_runtime_types.h @@ -10,8 +10,6 @@ RC_BEGIN_C_DECLS #ifndef RC_RUNTIME_H /* prevents pedantic redefiniton error */ -typedef struct lua_State lua_State; - typedef struct rc_trigger_t rc_trigger_t; typedef struct rc_lboard_t rc_lboard_t; typedef struct rc_richpresence_t rc_richpresence_t; @@ -29,7 +27,7 @@ typedef struct rc_value_t rc_value_t; * num_bytes is greater than 1, the value is read in little-endian from * memory. */ -typedef uint32_t(RC_CCONV *rc_peek_t)(uint32_t address, uint32_t num_bytes, void* ud); +typedef uint32_t(RC_CCONV* rc_peek_t)(uint32_t address, uint32_t num_bytes, void* ud); /*****************************************************************************\ | Memory References | @@ -70,15 +68,14 @@ typedef struct rc_memref_value_t { /* The last differing value of this memory reference. */ uint32_t prior; - /* The size of the value. */ + /* The size of the value. (RC_MEMSIZE_*) */ uint8_t size; /* True if the value changed this frame. */ uint8_t changed; - /* The value type of the value (for variables) */ + /* The value type of the value. (RC_VALUE_TYPE_*) */ uint8_t type; - /* True if the reference will be used in indirection. - * NOTE: This is actually a property of the rc_memref_t, but we put it here to save space */ - uint8_t is_indirect; + /* The type of memref (RC_MEMREF_TYPE_*) */ + uint8_t memref_type; } rc_memref_value_t; @@ -88,9 +85,6 @@ struct rc_memref_t { /* The memory address of this variable. */ uint32_t address; - - /* The next memory reference in the chain. */ - rc_memref_t* next; }; /*****************************************************************************\ @@ -103,7 +97,7 @@ enum { RC_OPERAND_DELTA, /* The value last known at this address. */ RC_OPERAND_CONST, /* A 32-bit unsigned integer. */ RC_OPERAND_FP, /* A floating point value. */ - RC_OPERAND_LUA, /* A Lua function that provides the value. */ + RC_OPERAND_FUNC, /* A function that provides the value. */ RC_OPERAND_PRIOR, /* The last differing value at this address. */ RC_OPERAND_BCD, /* The BCD-decoded value of a live address in RAM. */ RC_OPERAND_INVERTED, /* The twos-complement value of a live address in RAM. */ @@ -120,16 +114,19 @@ typedef struct rc_operand_t { /* A floating point value. */ double dbl; - - /* A reference to the Lua function that provides the value. */ - int luafunc; } value; - /* specifies which member of the value union is being used */ + /* specifies which member of the value union is being used (RC_OPERAND_*) */ uint8_t type; - /* the actual RC_MEMSIZE of the operand - memref.size may differ */ + /* the RC_MEMSIZE of the operand specified in the condition definition - memref.size may differ */ uint8_t size; + + /* specifies how to read the memref for some types (RC_OPERAND_*) */ + uint8_t memref_access_type; + + /* if set, this operand is combining the current condition with the previous one */ + uint8_t is_combining; } rc_operand_t; @@ -141,23 +138,16 @@ RC_EXPORT int RC_CCONV rc_operand_is_memref(const rc_operand_t* operand); /* types */ enum { - /* NOTE: this enum is ordered to optimize the switch statements in rc_test_condset_internal. the values may change between releases */ - - /* non-combining conditions (third switch) */ RC_CONDITION_STANDARD, /* this should always be 0 */ RC_CONDITION_PAUSE_IF, RC_CONDITION_RESET_IF, RC_CONDITION_MEASURED_IF, RC_CONDITION_TRIGGER, - RC_CONDITION_MEASURED, /* measured also appears in the first switch, so place it at the border between them */ - - /* modifiers (first switch) */ - RC_CONDITION_ADD_SOURCE, /* everything from this point on affects the condition after it */ + RC_CONDITION_MEASURED, + RC_CONDITION_ADD_SOURCE, RC_CONDITION_SUB_SOURCE, RC_CONDITION_ADD_ADDRESS, RC_CONDITION_REMEMBER, - - /* logic flags (second switch) */ RC_CONDITION_ADD_HITS, RC_CONDITION_SUB_HITS, RC_CONDITION_RESET_NEXT_IF, @@ -180,7 +170,10 @@ enum { RC_OPERATOR_XOR, RC_OPERATOR_MOD, RC_OPERATOR_ADD, - RC_OPERATOR_SUB + RC_OPERATOR_SUB, + + RC_OPERATOR_SUB_PARENT, /* internal use */ + RC_OPERATOR_INDIRECT_READ /* internal use */ }; typedef struct rc_condition_t rc_condition_t; @@ -204,10 +197,15 @@ struct rc_condition_t { /* The comparison operator to use. (RC_OPERATOR_*) */ uint8_t oper; /* operator is a reserved word in C++. */ - /* Set if the condition needs to processed as part of the "check if paused" pass. (bool) */ - uint8_t pause; - - /* Whether or not the condition evaluated true on the last check. (bool) */ + /* Will be non-zero if the condition evaluated true on the last check. + * - The lowest bit indicates whether the condition itself was true. + * - The second lowest bit will only ever be set on ResetIf conditions. + * If set, it indicates that the condition was responsible for resetting the + * trigger. A reset clears all hit counts, so the condition may not appear to + * be true just from looking at it (in which case the lower bit will be 0). + * Also, the condition might have only met its required_hits target though + * an AddHits chain which will have also been reset. + */ uint8_t is_true; /* Unique identifier of optimized comparator to use. (RC_PROCESSING_COMPARE_*) */ @@ -224,17 +222,32 @@ struct rc_condset_t { /* The next condition set in the chain. */ rc_condset_t* next; - /* The list of conditions in this condition set. */ + /* The first condition in this condition set. Then follow ->next chain. */ rc_condition_t* conditions; - /* True if any condition in the set is a pause condition. */ - uint8_t has_pause; + /* The number of pause conditions in this condition set. */ + /* The first pause condition is at "this + RC_ALIGN(sizeof(this)). */ + uint16_t num_pause_conditions; + /* The number of reset conditions in this condition set. */ + uint16_t num_reset_conditions; + + /* The number of hittarget conditions in this condition set. */ + uint16_t num_hittarget_conditions; + + /* The number of non-hittarget measured conditions in this condition set. */ + uint16_t num_measured_conditions; + + /* The number of other conditions in this condition set. */ + uint16_t num_other_conditions; + + /* The number of indirect conditions in this condition set. */ + uint16_t num_indirect_conditions; + + /* True if any condition in the set is a pause condition. */ + uint8_t has_pause; /* DEPRECATED - just check num_pause_conditions != 0 */ /* True if the set is currently paused. */ uint8_t is_paused; - - /* True if the set has indirect memory references. */ - uint8_t has_indirect_memrefs; }; /*****************************************************************************\ @@ -259,9 +272,6 @@ struct rc_trigger_t { /* The list of sub condition sets in this test. */ rc_condset_t* alternative; - /* The memory references required by the trigger. */ - rc_memref_t* memrefs; - /* The current state of the MEASURED condition. */ uint32_t measured_value; @@ -274,17 +284,17 @@ struct rc_trigger_t { /* True if at least one condition has a non-zero hit count */ uint8_t has_hits; - /* True if at least one condition has a non-zero required hit count */ - uint8_t has_required_hits; - /* True if the measured value should be displayed as a percentage */ uint8_t measured_as_percent; + + /* True if the trigger has its own rc_memrefs_t */ + uint8_t has_memrefs; }; RC_EXPORT int RC_CCONV rc_trigger_size(const char* memaddr); -RC_EXPORT rc_trigger_t* RC_CCONV rc_parse_trigger(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); -RC_EXPORT int RC_CCONV rc_evaluate_trigger(rc_trigger_t* trigger, rc_peek_t peek, void* ud, lua_State* L); -RC_EXPORT int RC_CCONV rc_test_trigger(rc_trigger_t* trigger, rc_peek_t peek, void* ud, lua_State* L); +RC_EXPORT rc_trigger_t* RC_CCONV rc_parse_trigger(void* buffer, const char* memaddr, void* unused_L, int unused_funcs_idx); +RC_EXPORT int RC_CCONV rc_evaluate_trigger(rc_trigger_t* trigger, rc_peek_t peek, void* ud, void* unused_L); +RC_EXPORT int RC_CCONV rc_test_trigger(rc_trigger_t* trigger, rc_peek_t peek, void* ud, void* unused_L); RC_EXPORT void RC_CCONV rc_reset_trigger(rc_trigger_t* self); /*****************************************************************************\ @@ -297,11 +307,11 @@ struct rc_value_t { /* The current value of the variable. */ rc_memref_value_t value; - /* The list of conditions to evaluate. */ - rc_condset_t* conditions; + /* True if the value has its own rc_memrefs_t */ + uint8_t has_memrefs; - /* The memory references required by the variable. */ - rc_memref_t* memrefs; + /* The list of possible values (traverse next chain, pick max). */ + rc_condset_t* conditions; /* The name of the variable. */ const char* name; @@ -311,8 +321,8 @@ struct rc_value_t { }; RC_EXPORT int RC_CCONV rc_value_size(const char* memaddr); -RC_EXPORT rc_value_t* RC_CCONV rc_parse_value(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); -RC_EXPORT int32_t RC_CCONV rc_evaluate_value(rc_value_t* value, rc_peek_t peek, void* ud, lua_State* L); +RC_EXPORT rc_value_t* RC_CCONV rc_parse_value(void* buffer, const char* memaddr, void* unused_L, int unused_funcs_idx); +RC_EXPORT int32_t RC_CCONV rc_evaluate_value(rc_value_t* value, rc_peek_t peek, void* ud, void* unused_L); /*****************************************************************************\ | Leaderboards | @@ -335,14 +345,14 @@ struct rc_lboard_t { rc_trigger_t cancel; rc_value_t value; rc_value_t* progress; - rc_memref_t* memrefs; uint8_t state; + uint8_t has_memrefs; }; RC_EXPORT int RC_CCONV rc_lboard_size(const char* memaddr); -RC_EXPORT rc_lboard_t* RC_CCONV rc_parse_lboard(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx); -RC_EXPORT int RC_CCONV rc_evaluate_lboard(rc_lboard_t* lboard, int32_t* value, rc_peek_t peek, void* peek_ud, lua_State* L); +RC_EXPORT rc_lboard_t* RC_CCONV rc_parse_lboard(void* buffer, const char* memaddr, void* unused_L, int unused_funcs_idx); +RC_EXPORT int RC_CCONV rc_evaluate_lboard(rc_lboard_t* lboard, int32_t* value, rc_peek_t peek, void* peek_ud, void* unused_L); RC_EXPORT void RC_CCONV rc_reset_lboard(rc_lboard_t* lboard); /*****************************************************************************\ @@ -370,7 +380,8 @@ enum { RC_FORMAT_TENS, RC_FORMAT_HUNDREDS, RC_FORMAT_THOUSANDS, - RC_FORMAT_UNSIGNED_VALUE + RC_FORMAT_UNSIGNED_VALUE, + RC_FORMAT_UNFORMATTED }; RC_EXPORT int RC_CCONV rc_parse_format(const char* format_str); @@ -406,7 +417,7 @@ struct rc_richpresence_display_part_t { rc_richpresence_display_part_t* next; const char* text; rc_richpresence_lookup_t* lookup; - rc_memref_value_t *value; + rc_operand_t value; uint8_t display_type; }; @@ -416,21 +427,22 @@ struct rc_richpresence_display_t { rc_trigger_t trigger; rc_richpresence_display_t* next; rc_richpresence_display_part_t* display; + uint8_t has_required_hits; }; struct rc_richpresence_t { rc_richpresence_display_t* first_display; rc_richpresence_lookup_t* first_lookup; - rc_memref_t* memrefs; - rc_value_t* variables; + rc_value_t* values; + uint8_t has_memrefs; }; RC_EXPORT int RC_CCONV rc_richpresence_size(const char* script); RC_EXPORT int RC_CCONV rc_richpresence_size_lines(const char* script, int* lines_read); -RC_EXPORT rc_richpresence_t* RC_CCONV rc_parse_richpresence(void* buffer, const char* script, lua_State* L, int funcs_ndx); -RC_EXPORT int RC_CCONV rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); -RC_EXPORT void RC_CCONV rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud, lua_State* L); -RC_EXPORT int RC_CCONV rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, lua_State* L); +RC_EXPORT rc_richpresence_t* RC_CCONV rc_parse_richpresence(void* buffer, const char* script, void* unused_L, int unused_funcs_idx); +RC_EXPORT int RC_CCONV rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, void* unused_L); +RC_EXPORT void RC_CCONV rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud, void* unused_L); +RC_EXPORT int RC_CCONV rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, void* unused_L); RC_EXPORT void RC_CCONV rc_reset_richpresence(rc_richpresence_t* self); RC_END_C_DECLS diff --git a/deps/rcheevos/src/rapi/rc_api_common.c b/deps/rcheevos/src/rapi/rc_api_common.c index 0b9d1f26b3..a7675ab751 100644 --- a/deps/rcheevos/src/rapi/rc_api_common.c +++ b/deps/rcheevos/src/rapi/rc_api_common.c @@ -13,8 +13,7 @@ #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; +rc_api_host_t g_host = { NULL, NULL }; /* --- rc_json --- */ @@ -318,6 +317,18 @@ static int rc_json_convert_error_code(const char* server_error_code) case 'i': if (strcmp(server_error_code, "invalid_credentials") == 0) return RC_INVALID_CREDENTIALS; + if (strcmp(server_error_code, "invalid_parameter") == 0) + return RC_INVALID_STATE; + break; + + case 'm': + if (strcmp(server_error_code, "missing_parameter") == 0) + return RC_INVALID_STATE; + break; + + case 'n': + if (strcmp(server_error_code, "not_found") == 0) + return RC_NOT_FOUND; break; default: @@ -696,6 +707,57 @@ int rc_json_get_string(const char** out, rc_buffer_t* buffer, const rc_json_fiel return 1; } +int rc_json_field_string_matches(const rc_json_field_t* field, const char* text) { + int is_quoted = 0; + const char* ptr = field->value_start; + if (!ptr) + return 0; + + if (*ptr == '"') { + is_quoted = 1; + ++ptr; + } + + while (ptr < field->value_end) { + if (*ptr != *text) { + if (*ptr != '\\') { + if (*ptr == '"' && is_quoted && (*text == '\0')) { + is_quoted = 0; + ++ptr; + continue; + } + + return 0; + } + + ++ptr; + switch (*ptr) { + case 'n': + if (*text != '\n') + return 0; + break; + case 'r': + if (*text != '\r') + return 0; + break; + case 't': + if (*text != '\t') + return 0; + break; + default: + if (*text != *ptr) + return 0; + break; + } + } + + ++text; + ++ptr; + } + + return !is_quoted && (*text == '\0'); +} + void rc_json_get_optional_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name, const char* default_value) { if (!rc_json_get_string(out, &response->buffer, field, field_name)) *out = default_value; @@ -1128,21 +1190,25 @@ void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* rc_url_builder_append_encoded_str(builder, value); } -void rc_api_url_build_dorequest_url(rc_api_request_t* request) { +void rc_api_url_build_dorequest_url(rc_api_request_t* request, const rc_api_host_t* host) { #define DOREQUEST_ENDPOINT "/dorequest.php" rc_buffer_init(&request->buffer); - if (!g_host) { + if (!host || !host->host) { request->url = RETROACHIEVEMENTS_HOST DOREQUEST_ENDPOINT; } else { const size_t endpoint_len = sizeof(DOREQUEST_ENDPOINT); - const size_t host_len = strlen(g_host); - const size_t url_len = host_len + endpoint_len; + const size_t host_len = strlen(host->host); + const size_t protocol_len = (strstr(host->host, "://")) ? 0 : 7; + const size_t url_len = protocol_len + host_len + endpoint_len; uint8_t* url = rc_buffer_reserve(&request->buffer, url_len); - memcpy(url, g_host, host_len); - memcpy(url + host_len, DOREQUEST_ENDPOINT, endpoint_len); + if (protocol_len) + memcpy(url, "http://", protocol_len); + + memcpy(url + protocol_len, host->host, host_len); + memcpy(url + protocol_len + host_len, DOREQUEST_ENDPOINT, endpoint_len); rc_buffer_consume(&request->buffer, url, url + url_len); request->url = (char*)url; @@ -1165,9 +1231,9 @@ int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, c /* --- Set Host --- */ -static void rc_api_update_host(char** host, const char* hostname) { +static void rc_api_update_host(const char** host, const char* hostname) { if (*host != NULL) - free(*host); + free((void*)*host); if (hostname != NULL) { if (strstr(hostname, "://")) { @@ -1196,11 +1262,15 @@ static void rc_api_update_host(char** host, const char* hostname) { } } +const char* rc_api_default_host(void) { + return RETROACHIEVEMENTS_HOST; +} + void rc_api_set_host(const char* hostname) { if (hostname && strcmp(hostname, RETROACHIEVEMENTS_HOST) == 0) hostname = NULL; - rc_api_update_host(&g_host, hostname); + rc_api_update_host(&g_host.host, hostname); if (!hostname) { /* also clear out the image hostname */ @@ -1214,24 +1284,45 @@ void rc_api_set_host(const char* hostname) { } void rc_api_set_image_host(const char* hostname) { - rc_api_update_host(&g_imagehost, hostname); + rc_api_update_host(&g_host.media_host, hostname); } /* --- Fetch Image --- */ int rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params) { + return rc_api_init_fetch_image_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_image_request_hosted(rc_api_request_t* request, const rc_api_fetch_image_request_t* api_params, const rc_api_host_t* host) { rc_api_url_builder_t builder; rc_buffer_init(&request->buffer); rc_url_builder_init(&builder, &request->buffer, 64); - if (g_imagehost) { - rc_url_builder_append(&builder, g_imagehost, strlen(g_imagehost)); + if (host && host->media_host) { + /* custom media host provided */ + if (!strstr(host->host, "://")) + rc_url_builder_append(&builder, "http://", 7); + rc_url_builder_append(&builder, host->media_host, strlen(host->media_host)); } - else if (g_host) { - rc_url_builder_append(&builder, g_host, strlen(g_host)); + else if (host && host->host) { + if (strcmp(host->host, RETROACHIEVEMENTS_HOST_NONSSL) == 0) { + /* if host specifically set to non-ssl host, and no media host provided, use non-ssl media host */ + rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST_NONSSL, sizeof(RETROACHIEVEMENTS_IMAGE_HOST_NONSSL) - 1); + } + else if (strcmp(host->host, RETROACHIEVEMENTS_HOST) == 0) { + /* if host specifically set to ssl host, and no media host provided, use media host */ + rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST, sizeof(RETROACHIEVEMENTS_IMAGE_HOST) - 1); + } + else { + /* custom host and no media host provided. assume custom host is also media host */ + if (!strstr(host->host, "://")) + rc_url_builder_append(&builder, "http://", 7); + rc_url_builder_append(&builder, host->host, strlen(host->host)); + } } else { + /* no custom host provided */ rc_url_builder_append(&builder, RETROACHIEVEMENTS_IMAGE_HOST, sizeof(RETROACHIEVEMENTS_IMAGE_HOST) - 1); } @@ -1270,3 +1361,19 @@ int rc_api_init_fetch_image_request(rc_api_request_t* request, const rc_api_fetc return builder.result; } + +const char* rc_api_build_avatar_url(rc_buffer_t* buffer, uint32_t image_type, const char* image_name) { + rc_api_fetch_image_request_t image_request; + rc_api_request_t request; + int result; + + 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) + return rc_buffer_strcpy(buffer, request.url); + + return NULL; +} diff --git a/deps/rcheevos/src/rapi/rc_api_common.h b/deps/rcheevos/src/rapi/rc_api_common.h index 538fdbba12..78e10d49b9 100644 --- a/deps/rcheevos/src/rapi/rc_api_common.h +++ b/deps/rcheevos/src/rapi/rc_api_common.h @@ -24,6 +24,8 @@ void rc_url_builder_init(rc_api_url_builder_t* builder, rc_buffer_t* buffer, siz 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); +extern rc_api_host_t g_host; + #define RC_JSON_NEW_FIELD(n) {NULL,NULL,n,sizeof(n)-1,0} typedef struct rc_json_field_t { @@ -66,6 +68,7 @@ int rc_json_get_required_array(uint32_t* num_entries, rc_json_field_t* array_fie int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, rc_json_iterator_t* iterator); int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field); int rc_json_get_object_string_length(const char* json); +int rc_json_field_string_matches(const rc_json_field_t* field, const char* text); void rc_json_extract_filename(rc_json_field_t* field); @@ -74,9 +77,12 @@ void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, uint32_t value); void rc_url_builder_append_str_param(rc_api_url_builder_t* builder, const char* param, const char* value); -void rc_api_url_build_dorequest_url(rc_api_request_t* request); +const char* rc_api_default_host(void); +void rc_api_url_build_dorequest_url(rc_api_request_t* request, const rc_api_host_t* host); int rc_api_url_build_dorequest(rc_api_url_builder_t* builder, const char* api, const char* username, const char* api_token); +const char* rc_api_build_avatar_url(rc_buffer_t* buffer, uint32_t image_type, const char* image_name); + RC_END_C_DECLS #endif /* RC_API_COMMON_H */ diff --git a/deps/rcheevos/src/rapi/rc_api_info.c b/deps/rcheevos/src/rapi/rc_api_info.c index 0339f256d4..84423ae0e2 100644 --- a/deps/rcheevos/src/rapi/rc_api_info.c +++ b/deps/rcheevos/src/rapi/rc_api_info.c @@ -1,5 +1,6 @@ #include "rc_api_info.h" #include "rc_api_common.h" +#include "rc_api_runtime.h" #include "rc_runtime_types.h" @@ -11,9 +12,15 @@ /* --- Fetch Achievement Info --- */ int rc_api_init_fetch_achievement_info_request(rc_api_request_t* request, const rc_api_fetch_achievement_info_request_t* api_params) { + return rc_api_init_fetch_achievement_info_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_achievement_info_request_hosted(rc_api_request_t* request, + const rc_api_fetch_achievement_info_request_t* api_params, + const rc_api_host_t* host) { rc_api_url_builder_t builder; - rc_api_url_build_dorequest_url(request); + rc_api_url_build_dorequest_url(request, host); if (api_params->achievement_id == 0) return RC_INVALID_STATE; @@ -73,7 +80,8 @@ int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achieveme rc_json_field_t entry_fields[] = { RC_JSON_NEW_FIELD("User"), - RC_JSON_NEW_FIELD("DateAwarded") + RC_JSON_NEW_FIELD("DateAwarded"), + RC_JSON_NEW_FIELD("AvatarUrl") }; memset(response, 0, sizeof(*response)); @@ -116,6 +124,10 @@ int rc_api_process_fetch_achievement_info_server_response(rc_api_fetch_achieveme return RC_MISSING_VALUE; entry->awarded = (time_t)timet; + rc_json_get_optional_string(&entry->avatar_url, &response->response, &entry_fields[2], "AvatarUrl", NULL); + if (!entry->avatar_url) + entry->avatar_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_USER, entry->username); + ++entry; } } @@ -130,9 +142,15 @@ void rc_api_destroy_fetch_achievement_info_response(rc_api_fetch_achievement_inf /* --- Fetch Leaderboard Info --- */ int rc_api_init_fetch_leaderboard_info_request(rc_api_request_t* request, const rc_api_fetch_leaderboard_info_request_t* api_params) { + return rc_api_init_fetch_leaderboard_info_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_leaderboard_info_request_hosted(rc_api_request_t* request, + const rc_api_fetch_leaderboard_info_request_t* api_params, + const rc_api_host_t* host) { rc_api_url_builder_t builder; - rc_api_url_build_dorequest_url(request); + rc_api_url_build_dorequest_url(request, host); if (api_params->leaderboard_id == 0) return RC_INVALID_STATE; @@ -191,13 +209,6 @@ int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboa RC_JSON_NEW_FIELD("LBUpdated"), RC_JSON_NEW_FIELD("Entries"), /* array */ RC_JSON_NEW_FIELD("TotalEntries") - /* unused fields - 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[] = { @@ -205,7 +216,8 @@ int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboa RC_JSON_NEW_FIELD("Rank"), RC_JSON_NEW_FIELD("Index"), RC_JSON_NEW_FIELD("Score"), - RC_JSON_NEW_FIELD("DateSubmitted") + RC_JSON_NEW_FIELD("DateSubmitted"), + RC_JSON_NEW_FIELD("AvatarUrl") }; memset(response, 0, sizeof(*response)); @@ -281,6 +293,10 @@ int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboa return RC_MISSING_VALUE; entry->submitted = (time_t)timet; + rc_json_get_optional_string(&entry->avatar_url, &response->response, &entry_fields[5], "AvatarUrl", NULL); + if (!entry->avatar_url) + entry->avatar_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_USER, entry->username); + ++entry; } } @@ -295,9 +311,15 @@ void rc_api_destroy_fetch_leaderboard_info_response(rc_api_fetch_leaderboard_inf /* --- Fetch Games List --- */ int rc_api_init_fetch_games_list_request(rc_api_request_t* request, const rc_api_fetch_games_list_request_t* api_params) { + return rc_api_init_fetch_games_list_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_games_list_request_hosted(rc_api_request_t* request, + const rc_api_fetch_games_list_request_t* api_params, + const rc_api_host_t* host) { rc_api_url_builder_t builder; - rc_api_url_build_dorequest_url(request); + rc_api_url_build_dorequest_url(request, host); if (api_params->console_id == 0) return RC_INVALID_STATE; @@ -380,11 +402,17 @@ void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t /* --- Fetch Game Titles --- */ int rc_api_init_fetch_game_titles_request(rc_api_request_t* request, const rc_api_fetch_game_titles_request_t* api_params) { + return rc_api_init_fetch_game_titles_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_game_titles_request_hosted(rc_api_request_t* request, + const rc_api_fetch_game_titles_request_t* api_params, + const rc_api_host_t* host) { rc_api_url_builder_t builder; char num[16]; uint32_t i; - rc_api_url_build_dorequest_url(request); + rc_api_url_build_dorequest_url(request, host); if (api_params->num_game_ids == 0) return RC_INVALID_STATE; @@ -464,3 +492,91 @@ int rc_api_process_fetch_game_titles_server_response(rc_api_fetch_game_titles_re void rc_api_destroy_fetch_game_titles_response(rc_api_fetch_game_titles_response_t* response) { rc_buffer_destroy(&response->response.buffer); } + +/* --- Fetch Game Hashes --- */ + +int rc_api_init_fetch_hash_library_request(rc_api_request_t* request, + const rc_api_fetch_hash_library_request_t* api_params) +{ + return rc_api_init_fetch_hash_library_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_hash_library_request_hosted(rc_api_request_t* request, + const rc_api_fetch_hash_library_request_t* api_params, + const rc_api_host_t* host) +{ + rc_api_url_builder_t builder; + rc_api_url_build_dorequest_url(request, host); + + /* note: unauthenticated request */ + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "hashlibrary"); + if (api_params->console_id != 0) + 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_hash_library_server_response(rc_api_fetch_hash_library_response_t* response, + const rc_api_server_response_t* server_response) +{ + rc_api_hash_library_entry_t* entry; + rc_json_iterator_t iterator; + rc_json_field_t field; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("MD5List"), + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = + rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK) + return result; + + if (!fields[2].value_start) { + /* call rc_json_get_required_object to generate the error message */ + rc_json_get_required_object(NULL, 0, &response->response, &fields[2], "MD5List"); + return RC_MISSING_VALUE; + } + + response->num_entries = fields[2].array_size; + if (response->num_entries > 0) { + rc_buffer_reserve(&response->response.buffer, response->num_entries * (33 + sizeof(rc_api_hash_library_entry_t))); + + response->entries = (rc_api_hash_library_entry_t*)rc_buffer_alloc( + &response->response.buffer, response->num_entries * sizeof(rc_api_hash_library_entry_t)); + if (!response->entries) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = fields[2].value_start; + iterator.end = fields[2].value_end; + + entry = response->entries; + while (rc_json_get_next_object_field(&iterator, &field)) { + entry->hash = rc_buffer_strncpy(&response->response.buffer, field.name, field.name_len); + + field.name = ""; + if (!rc_json_get_unum(&entry->game_id, &field, "")) + return RC_MISSING_VALUE; + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_hash_library_response(rc_api_fetch_hash_library_response_t* response) +{ + rc_buffer_destroy(&response->response.buffer); +} diff --git a/deps/rcheevos/src/rapi/rc_api_runtime.c b/deps/rcheevos/src/rapi/rc_api_runtime.c index 0ad58f01fe..99a001db2f 100644 --- a/deps/rcheevos/src/rapi/rc_api_runtime.c +++ b/deps/rcheevos/src/rapi/rc_api_runtime.c @@ -13,9 +13,15 @@ /* --- Resolve Hash --- */ int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params) { + return rc_api_init_resolve_hash_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_resolve_hash_request_hosted(rc_api_request_t* request, + const rc_api_resolve_hash_request_t* api_params, + const rc_api_host_t* host) { rc_api_url_builder_t builder; - rc_api_url_build_dorequest_url(request); + rc_api_url_build_dorequest_url(request, host); if (!api_params->game_hash || !*api_params->game_hash) return RC_INVALID_STATE; @@ -65,16 +71,26 @@ void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* respon /* --- Fetch Game Data --- */ int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params) { + return rc_api_init_fetch_game_data_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_game_data_request_hosted(rc_api_request_t* request, + const rc_api_fetch_game_data_request_t* api_params, + const rc_api_host_t* host) { rc_api_url_builder_t builder; - rc_api_url_build_dorequest_url(request); + rc_api_url_build_dorequest_url(request, host); - if (api_params->game_id == 0) + if (api_params->game_id == 0 && (!api_params->game_hash || !api_params->game_hash[0])) return RC_INVALID_STATE; rc_url_builder_init(&builder, &request->buffer, 48); if (rc_api_url_build_dorequest(&builder, "patch", api_params->username, api_params->api_token)) { - rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + if (api_params->game_id) + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + else + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + request->post_data = rc_url_builder_finalize(&builder); request->content_type = RC_CONTENT_TYPE_URLENCODED; } @@ -92,48 +108,13 @@ int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* r return rc_api_process_fetch_game_data_server_response(response, &response_obj); } -static int rc_parse_achievement_type(const char* type) -{ - if (strcmp(type, "missable") == 0) - return RC_ACHIEVEMENT_TYPE_MISSABLE; - - if (strcmp(type, "win_condition") == 0) - return RC_ACHIEVEMENT_TYPE_WIN; - - if (strcmp(type, "progression") == 0) - return RC_ACHIEVEMENT_TYPE_PROGRESSION; - - return RC_ACHIEVEMENT_TYPE_STANDARD; -} - -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 array_field; +static int rc_api_process_fetch_game_data_achievements(rc_api_response_t* response, rc_api_achievement_definition_t* achievement, rc_json_field_t* array_field) { rc_json_iterator_t iterator; const char* last_author = ""; const char* last_author_field = ""; size_t last_author_len = 0; - size_t len; uint32_t timet; - int result; - char format[16]; - - rc_json_field_t fields[] = { - RC_JSON_NEW_FIELD("Success"), - RC_JSON_NEW_FIELD("Error"), - RC_JSON_NEW_FIELD("PatchData") /* nested object */ - }; - - rc_json_field_t patchdata_fields[] = { - 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 */ - }; + size_t len; rc_json_field_t achievement_fields[] = { RC_JSON_NEW_FIELD("ID"), @@ -148,9 +129,107 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon RC_JSON_NEW_FIELD("Modified"), RC_JSON_NEW_FIELD("Type"), RC_JSON_NEW_FIELD("Rarity"), - RC_JSON_NEW_FIELD("RarityHardcore") + RC_JSON_NEW_FIELD("RarityHardcore"), + RC_JSON_NEW_FIELD("BadgeURL"), + RC_JSON_NEW_FIELD("BadgeLockedURL") }; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field->value_start; + iterator.end = array_field->value_end; + + while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&achievement->id, response, &achievement_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->title, response, &achievement_fields[1], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->description, response, &achievement_fields[2], "Description")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&achievement->category, response, &achievement_fields[3], "Flags")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&achievement->points, response, &achievement_fields[4], "Points")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->definition, response, &achievement_fields[5], "MemAddr")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&achievement->badge_name, response, &achievement_fields[7], "BadgeName")) + return RC_MISSING_VALUE; + + rc_json_get_optional_string(&achievement->badge_url, response, &achievement_fields[13], "BadgeURL", ""); + if (!achievement->badge_url[0]) + achievement->badge_url = rc_api_build_avatar_url(&response->buffer, RC_IMAGE_TYPE_ACHIEVEMENT, achievement->badge_name); + + rc_json_get_optional_string(&achievement->badge_locked_url, response, &achievement_fields[14], "BadgeLockedURL", ""); + if (!achievement->badge_locked_url[0]) + achievement->badge_locked_url = rc_api_build_avatar_url(&response->buffer, RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, achievement->badge_name); + + len = achievement_fields[6].value_end - achievement_fields[6].value_start; + if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) { + achievement->author = last_author; + } + else { + if (!rc_json_get_required_string(&achievement->author, response, &achievement_fields[6], "Author")) + return RC_MISSING_VALUE; + + if (achievement->author == NULL) { + /* ensure we don't pass NULL out to client */ + last_author = achievement->author = ""; + last_author_len = 0; + } else { + last_author = achievement->author; + last_author_field = achievement_fields[6].value_start; + last_author_len = len; + } + } + + if (!rc_json_get_required_unum(&timet, response, &achievement_fields[8], "Created")) + return RC_MISSING_VALUE; + achievement->created = (time_t)timet; + if (!rc_json_get_required_unum(&timet, response, &achievement_fields[9], "Modified")) + return RC_MISSING_VALUE; + achievement->updated = (time_t)timet; + + if (rc_json_field_string_matches(&achievement_fields[10], "")) + achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD; + else if (rc_json_field_string_matches(&achievement_fields[10], "progression")) + achievement->type = RC_ACHIEVEMENT_TYPE_PROGRESSION; + else if (rc_json_field_string_matches(&achievement_fields[10], "missable")) + achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE; + else if (rc_json_field_string_matches(&achievement_fields[10], "win_condition")) + achievement->type = RC_ACHIEVEMENT_TYPE_WIN; + else + achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD; + + /* legacy support : if title contains[m], change type to missable and remove[m] from title */ + if (memcmp(achievement->title, "[m]", 3) == 0) { + len = 3; + while (achievement->title[len] == ' ') + ++len; + achievement->title += len; + achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE; + } + else if (achievement_fields[1].value_end && memcmp(achievement_fields[1].value_end - 4, "[m]", 3) == 0) { + len = strlen(achievement->title) - 3; + while (achievement->title[len - 1] == ' ') + --len; + ((char*)achievement->title)[len] = '\0'; + achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE; + } + + rc_json_get_optional_float(&achievement->rarity, &achievement_fields[11], "Rarity", 100.0); + rc_json_get_optional_float(&achievement->rarity_hardcore, &achievement_fields[12], "RarityHardcore", 100.0); + + ++achievement; + } + + return RC_OK; +} + +static int rc_api_process_fetch_game_data_leaderboards(rc_api_response_t* response, rc_api_leaderboard_definition_t* leaderboard, rc_json_field_t* array_field) { + rc_json_iterator_t iterator; + size_t len; + int result; + char format[16]; + rc_json_field_t leaderboard_fields[] = { RC_JSON_NEW_FIELD("ID"), RC_JSON_NEW_FIELD("Title"), @@ -161,6 +240,65 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon RC_JSON_NEW_FIELD("Hidden") }; + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field->value_start; + iterator.end = array_field->value_end; + + while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&leaderboard->id, response, &leaderboard_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&leaderboard->title, response, &leaderboard_fields[1], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&leaderboard->description, response, &leaderboard_fields[2], "Description")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&leaderboard->definition, response, &leaderboard_fields[3], "Mem")) + return RC_MISSING_VALUE; + rc_json_get_optional_bool(&result, &leaderboard_fields[5], "LowerIsBetter", 0); + leaderboard->lower_is_better = (uint8_t)result; + rc_json_get_optional_bool(&result, &leaderboard_fields[6], "Hidden", 0); + leaderboard->hidden = (uint8_t)result; + + if (!leaderboard_fields[4].value_end) + return RC_MISSING_VALUE; + len = leaderboard_fields[4].value_end - leaderboard_fields[4].value_start - 2; + if (len < sizeof(format) - 1) { + memcpy(format, leaderboard_fields[4].value_start + 1, len); + format[len] = '\0'; + leaderboard->format = rc_parse_format(format); + } + else { + leaderboard->format = RC_FORMAT_VALUE; + } + + ++leaderboard; + } + + return RC_OK; +} + +int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t array_field; + size_t len; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Code"), + RC_JSON_NEW_FIELD("PatchData") /* nested object */ + }; + + rc_json_field_t patchdata_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("ConsoleID"), + RC_JSON_NEW_FIELD("ImageIcon"), + RC_JSON_NEW_FIELD("ImageIconURL"), + RC_JSON_NEW_FIELD("RichPresencePatch"), + RC_JSON_NEW_FIELD("Achievements"), /* array */ + RC_JSON_NEW_FIELD("Leaderboards"), /* array */ + }; + memset(response, 0, sizeof(*response)); rc_buffer_init(&response->response.buffer); @@ -168,7 +306,7 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon if (result != RC_OK || !response->response.succeeded) return result; - if (!rc_json_get_required_object(patchdata_fields, sizeof(patchdata_fields) / sizeof(patchdata_fields[0]), &response->response, &fields[2], "PatchData")) + if (!rc_json_get_required_object(patchdata_fields, sizeof(patchdata_fields) / sizeof(patchdata_fields[0]), &response->response, &fields[3], "PatchData")) return RC_MISSING_VALUE; if (!rc_json_get_required_unum(&response->id, &response->response, &patchdata_fields[0], "ID")) @@ -181,26 +319,29 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon /* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */ rc_json_extract_filename(&patchdata_fields[3]); rc_json_get_optional_string(&response->image_name, &response->response, &patchdata_fields[3], "ImageIcon", ""); + rc_json_get_optional_string(&response->image_url, &response->response, &patchdata_fields[4], "ImageIconURL", ""); + if (!response->image_url[0]) + response->image_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_GAME, response->image_name); /* estimate the amount of space necessary to store the rich presence script, achievements, and leaderboards. determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation) and add space for the structures. */ - len = patchdata_fields[4].value_end - patchdata_fields[4].value_start; /* rich presence */ + len = patchdata_fields[5].value_end - patchdata_fields[5].value_start; /* rich presence */ - len += (patchdata_fields[5].value_end - patchdata_fields[5].value_start) - /* achievements */ - patchdata_fields[5].array_size * (130 - sizeof(rc_api_achievement_definition_t)); + len += (patchdata_fields[6].value_end - patchdata_fields[6].value_start) - /* achievements */ + patchdata_fields[6].array_size * (80 - sizeof(rc_api_achievement_definition_t)); - len += (patchdata_fields[6].value_end - patchdata_fields[6].value_start) - /* leaderboards */ - patchdata_fields[6].array_size * (60 - sizeof(rc_api_leaderboard_definition_t)); + len += (patchdata_fields[7].value_end - patchdata_fields[7].value_start) - /* leaderboards */ + patchdata_fields[7].array_size * (60 - sizeof(rc_api_leaderboard_definition_t)); rc_buffer_reserve(&response->response.buffer, len); /* end estimation */ - rc_json_get_optional_string(&response->rich_presence_script, &response->response, &patchdata_fields[4], "RichPresencePatch", ""); + rc_json_get_optional_string(&response->rich_presence_script, &response->response, &patchdata_fields[5], "RichPresencePatch", ""); if (!response->rich_presence_script) response->rich_presence_script = ""; - if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[5], "Achievements")) + if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[6], "Achievements")) return RC_MISSING_VALUE; if (response->num_achievements) { @@ -208,87 +349,12 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon 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")) - return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&achievement->title, &response->response, &achievement_fields[1], "Title")) - return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&achievement->description, &response->response, &achievement_fields[2], "Description")) - return RC_MISSING_VALUE; - if (!rc_json_get_required_unum(&achievement->category, &response->response, &achievement_fields[3], "Flags")) - return RC_MISSING_VALUE; - if (!rc_json_get_required_unum(&achievement->points, &response->response, &achievement_fields[4], "Points")) - return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&achievement->definition, &response->response, &achievement_fields[5], "MemAddr")) - return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&achievement->badge_name, &response->response, &achievement_fields[7], "BadgeName")) - return RC_MISSING_VALUE; - - len = achievement_fields[6].value_end - achievement_fields[6].value_start; - if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) { - achievement->author = last_author; - } - else { - if (!rc_json_get_required_string(&achievement->author, &response->response, &achievement_fields[6], "Author")) - return RC_MISSING_VALUE; - - if (achievement->author == NULL) { - /* ensure we don't pass NULL out to client */ - last_author = achievement->author = ""; - last_author_len = 0; - } else { - last_author = achievement->author; - last_author_field = achievement_fields[6].value_start; - last_author_len = len; - } - } - - if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[8], "Created")) - return RC_MISSING_VALUE; - achievement->created = (time_t)timet; - if (!rc_json_get_required_unum(&timet, &response->response, &achievement_fields[9], "Modified")) - return RC_MISSING_VALUE; - achievement->updated = (time_t)timet; - - achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD; - if (achievement_fields[10].value_end) { - len = achievement_fields[10].value_end - achievement_fields[10].value_start - 2; - if (len < sizeof(format) - 1) { - memcpy(format, achievement_fields[10].value_start + 1, len); - format[len] = '\0'; - achievement->type = rc_parse_achievement_type(format); - } - } - - /* legacy support : if title contains[m], change type to missable and remove[m] from title */ - if (memcmp(achievement->title, "[m]", 3) == 0) { - len = 3; - while (achievement->title[len] == ' ') - ++len; - achievement->title += len; - achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE; - } - else if (achievement_fields[1].value_end && memcmp(achievement_fields[1].value_end - 4, "[m]", 3) == 0) { - len = strlen(achievement->title) - 3; - while (achievement->title[len - 1] == ' ') - --len; - ((char*)achievement->title)[len] = '\0'; - achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE; - } - - rc_json_get_optional_float(&achievement->rarity, &achievement_fields[11], "Rarity", 100.0); - rc_json_get_optional_float(&achievement->rarity_hardcore, &achievement_fields[12], "RarityHardcore", 100.0); - - ++achievement; - } + result = rc_api_process_fetch_game_data_achievements(&response->response, response->achievements, &array_field); + if (result != RC_OK) + return result; } - if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[6], "Leaderboards")) + if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[7], "Leaderboards")) return RC_MISSING_VALUE; if (response->num_leaderboards) { @@ -296,39 +362,9 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon 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")) - return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&leaderboard->title, &response->response, &leaderboard_fields[1], "Title")) - return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&leaderboard->description, &response->response, &leaderboard_fields[2], "Description")) - return RC_MISSING_VALUE; - if (!rc_json_get_required_string(&leaderboard->definition, &response->response, &leaderboard_fields[3], "Mem")) - return RC_MISSING_VALUE; - rc_json_get_optional_bool(&result, &leaderboard_fields[5], "LowerIsBetter", 0); - leaderboard->lower_is_better = (uint8_t)result; - rc_json_get_optional_bool(&result, &leaderboard_fields[6], "Hidden", 0); - leaderboard->hidden = (uint8_t)result; - - if (!leaderboard_fields[4].value_end) - return RC_MISSING_VALUE; - len = leaderboard_fields[4].value_end - leaderboard_fields[4].value_start - 2; - if (len < sizeof(format) - 1) { - memcpy(format, leaderboard_fields[4].value_start + 1, len); - format[len] = '\0'; - leaderboard->format = rc_parse_format(format); - } - else { - leaderboard->format = RC_FORMAT_VALUE; - } - - ++leaderboard; - } + result = rc_api_process_fetch_game_data_leaderboards(&response->response, response->leaderboards, &array_field); + if (result != RC_OK) + return result; } return RC_OK; @@ -338,12 +374,205 @@ void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* rc_buffer_destroy(&response->response.buffer); } +/* --- Fetch Game Sets --- */ + +int rc_api_init_fetch_game_sets_request(rc_api_request_t* request, const rc_api_fetch_game_sets_request_t* api_params) { + return rc_api_init_fetch_game_sets_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_game_sets_request_hosted(rc_api_request_t* request, + const rc_api_fetch_game_sets_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (!api_params->game_id && (!api_params->game_hash || !api_params->game_hash[0])) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "achievementsets", api_params->username, api_params->api_token)) { + if (api_params->game_id) + rc_url_builder_append_unum_param(&builder, "g", api_params->game_id); + else + rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +static int rc_api_process_fetch_game_sets_achievement_sets(rc_api_fetch_game_sets_response_t* response, + rc_api_achievement_set_definition_t* subset, + rc_json_field_t* subset_array_field) { + rc_json_iterator_t iterator; + rc_json_field_t array_field; + size_t len; + int result; + + rc_json_field_t subset_fields[] = { + RC_JSON_NEW_FIELD("AchievementSetId"), + RC_JSON_NEW_FIELD("GameId"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("Type"), + RC_JSON_NEW_FIELD("ImageIconUrl"), + RC_JSON_NEW_FIELD("Achievements"), /* array */ + RC_JSON_NEW_FIELD("Leaderboards") /* array */ + }; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = subset_array_field->value_start; + iterator.end = subset_array_field->value_end; + + while (rc_json_get_array_entry_object(subset_fields, sizeof(subset_fields) / sizeof(subset_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&subset->id, &response->response, &subset_fields[0], "AchievementSetId")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&subset->game_id, &response->response, &subset_fields[1], "GameId")) + return RC_MISSING_VALUE; + + if (!rc_json_get_required_string(&subset->title, &response->response, &subset_fields[2], "Title")) + return RC_MISSING_VALUE; + if (!subset->title || !subset->title[0]) + subset->title = response->title; + + if (rc_json_field_string_matches(&subset_fields[3], "core")) + subset->type = RC_ACHIEVEMENT_SET_TYPE_CORE; + else if (rc_json_field_string_matches(&subset_fields[3], "bonus")) + subset->type = RC_ACHIEVEMENT_SET_TYPE_BONUS; + else if (rc_json_field_string_matches(&subset_fields[3], "specialty")) + subset->type = RC_ACHIEVEMENT_SET_TYPE_SPECIALTY; + else if (rc_json_field_string_matches(&subset_fields[3], "exclusive")) + subset->type = RC_ACHIEVEMENT_SET_TYPE_EXCLUSIVE; + else + subset->type = RC_ACHIEVEMENT_SET_TYPE_BONUS; + + if (rc_json_field_string_matches(&subset_fields[4], response->image_url)) { + subset->image_url = response->image_url; + subset->image_name = response->image_name; + } + else { + if (!rc_json_get_required_string(&subset->image_url, &response->response, &subset_fields[4], "ImageIconUrl")) + return RC_MISSING_VALUE; + rc_json_extract_filename(&subset_fields[4]); + rc_json_get_optional_string(&subset->image_name, &response->response, &subset_fields[4], "ImageIconUrl", ""); + } + + /* estimate the amount of space necessary to store the achievements, and leaderboards. + determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation) + and add space for the structures. */ + len = (subset_fields[5].value_end - subset_fields[5].value_start) - /* achievements */ + subset_fields[5].array_size * (80 - sizeof(rc_api_achievement_definition_t)); + len += (subset_fields[6].value_end - subset_fields[6].value_start) - /* leaderboards */ + subset_fields[6].array_size * (60 - sizeof(rc_api_leaderboard_definition_t)); + + rc_buffer_reserve(&response->response.buffer, len); + /* end estimation */ + + if (!rc_json_get_required_array(&subset->num_achievements, &array_field, &response->response, &subset_fields[5], "Achievements")) + return RC_MISSING_VALUE; + + if (subset->num_achievements) { + subset->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_achievements * sizeof(rc_api_achievement_definition_t)); + if (!subset->achievements) + return RC_OUT_OF_MEMORY; + + result = rc_api_process_fetch_game_data_achievements(&response->response, subset->achievements, &array_field); + if (result != RC_OK) + return result; + } + + if (!rc_json_get_required_array(&subset->num_leaderboards, &array_field, &response->response, &subset_fields[6], "Leaderboards")) + return RC_MISSING_VALUE; + + if (subset->num_leaderboards) { + subset->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_leaderboards * sizeof(rc_api_leaderboard_definition_t)); + if (!subset->leaderboards) + return RC_OUT_OF_MEMORY; + + result = rc_api_process_fetch_game_data_leaderboards(&response->response, subset->leaderboards, &array_field); + if (result != RC_OK) + return result; + } + + ++subset; + } + + return RC_OK; +} + +int rc_api_process_fetch_game_sets_server_response(rc_api_fetch_game_sets_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t array_field; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Code"), + RC_JSON_NEW_FIELD("GameId"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("ConsoleId"), + RC_JSON_NEW_FIELD("ImageIconUrl"), + RC_JSON_NEW_FIELD("RichPresenceGameId"), + RC_JSON_NEW_FIELD("RichPresencePatch"), + RC_JSON_NEW_FIELD("Sets") /* array */ + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_unum(&response->id, &response->response, &fields[3], "GameId")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&response->title, &response->response, &fields[4], "Title")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->console_id, &response->response, &fields[5], "ConsoleId")) + return RC_MISSING_VALUE; + + rc_json_get_required_string(&response->image_url, &response->response, &fields[6], "ImageIconUrl"); + rc_json_extract_filename(&fields[6]); + rc_json_get_required_string(&response->image_name, &response->response, &fields[6], "ImageIconUrl"); + + rc_json_get_optional_unum(&response->session_game_id, &fields[7], "RichPresenceGameId", response->id); + + rc_json_get_optional_string(&response->rich_presence_script, &response->response, &fields[8], "RichPresencePatch", ""); + if (!response->rich_presence_script) + response->rich_presence_script = ""; + + rc_json_get_optional_array(&response->num_sets, &array_field, &fields[9], "Sets"); + if (response->num_sets) { + response->sets = (rc_api_achievement_set_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_sets * sizeof(rc_api_achievement_set_definition_t)); + if (!response->sets) + return RC_OUT_OF_MEMORY; + + result = rc_api_process_fetch_game_sets_achievement_sets(response, response->sets, &array_field); + if (result != RC_OK) + return result; + } + + return RC_OK; +} + +void rc_api_destroy_fetch_game_sets_response(rc_api_fetch_game_sets_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + /* --- Ping --- */ int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params) { + return rc_api_init_ping_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_ping_request_hosted(rc_api_request_t* request, + const rc_api_ping_request_t* api_params, + const rc_api_host_t* host) { rc_api_url_builder_t builder; - rc_api_url_build_dorequest_url(request); + rc_api_url_build_dorequest_url(request, host); if (api_params->game_id == 0) return RC_INVALID_STATE; @@ -396,12 +625,18 @@ void rc_api_destroy_ping_response(rc_api_ping_response_t* response) { /* --- Award Achievement --- */ int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params) { + return rc_api_init_award_achievement_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_award_achievement_request_hosted(rc_api_request_t* request, + const rc_api_award_achievement_request_t* api_params, + const rc_api_host_t* host) { rc_api_url_builder_t builder; char buffer[33]; md5_state_t md5; md5_byte_t digest[16]; - rc_api_url_build_dorequest_url(request); + rc_api_url_build_dorequest_url(request, host); if (api_params->achievement_id == 0) return RC_INVALID_STATE; @@ -497,12 +732,18 @@ void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response /* --- Submit Leaderboard Entry --- */ int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params) { + return rc_api_init_submit_lboard_entry_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_submit_lboard_entry_request_hosted(rc_api_request_t* request, + const rc_api_submit_lboard_entry_request_t* api_params, + const rc_api_host_t* host) { rc_api_url_builder_t builder; char buffer[33]; md5_state_t md5; md5_byte_t digest[16]; - rc_api_url_build_dorequest_url(request); + rc_api_url_build_dorequest_url(request, host); if (api_params->leaderboard_id == 0) return RC_INVALID_STATE; diff --git a/deps/rcheevos/src/rapi/rc_api_user.c b/deps/rcheevos/src/rapi/rc_api_user.c index 0349f4a584..323756cfd1 100644 --- a/deps/rcheevos/src/rapi/rc_api_user.c +++ b/deps/rcheevos/src/rapi/rc_api_user.c @@ -1,16 +1,25 @@ #include "rc_api_user.h" #include "rc_api_common.h" +#include "rc_api_runtime.h" +#include "rc_consoles.h" #include "../rc_version.h" +#include #include /* --- Login --- */ int rc_api_init_login_request(rc_api_request_t* request, const rc_api_login_request_t* api_params) { + return rc_api_init_login_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_login_request_hosted(rc_api_request_t* request, + const rc_api_login_request_t* api_params, + const rc_api_host_t* host) { rc_api_url_builder_t builder; - rc_api_url_build_dorequest_url(request); + rc_api_url_build_dorequest_url(request, host); if (!api_params->username || !*api_params->username) return RC_INVALID_STATE; @@ -53,7 +62,7 @@ int rc_api_process_login_server_response(rc_api_login_response_t* response, cons RC_JSON_NEW_FIELD("Score"), RC_JSON_NEW_FIELD("SoftcoreScore"), RC_JSON_NEW_FIELD("Messages"), - RC_JSON_NEW_FIELD("DisplayName") + RC_JSON_NEW_FIELD("AvatarUrl") }; memset(response, 0, sizeof(*response)); @@ -72,7 +81,14 @@ int rc_api_process_login_server_response(rc_api_login_response_t* response, cons rc_json_get_optional_unum(&response->score_softcore, &fields[6], "SoftcoreScore", 0); rc_json_get_optional_unum(&response->num_unread_messages, &fields[7], "Messages", 0); - rc_json_get_optional_string(&response->display_name, &response->response, &fields[8], "DisplayName", response->username); + /* For the highest level of backwards compatibility, we have decided to just send the + * display_name back to the client as the "case-corrected username", and allow it to + * be used as the Username field for the other APIs. */ + response->display_name = response->username; + + rc_json_get_optional_string(&response->avatar_url, &response->response, &fields[8], "AvatarUrl", NULL); + if (!response->avatar_url) + response->avatar_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_USER, response->username); return RC_OK; } @@ -84,9 +100,15 @@ void rc_api_destroy_login_response(rc_api_login_response_t* response) { /* --- Start Session --- */ int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_start_session_request_t* api_params) { + return rc_api_init_start_session_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_start_session_request_hosted(rc_api_request_t* request, + const rc_api_start_session_request_t* api_params, + const rc_api_host_t* host) { rc_api_url_builder_t builder; - rc_api_url_build_dorequest_url(request); + rc_api_url_build_dorequest_url(request, host); if (api_params->game_id == 0) return RC_INVALID_STATE; @@ -201,9 +223,15 @@ void rc_api_destroy_start_session_response(rc_api_start_session_response_t* resp /* --- Fetch User Unlocks --- */ int rc_api_init_fetch_user_unlocks_request(rc_api_request_t* request, const rc_api_fetch_user_unlocks_request_t* api_params) { + return rc_api_init_fetch_user_unlocks_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_user_unlocks_request_hosted(rc_api_request_t* request, + const rc_api_fetch_user_unlocks_request_t* api_params, + const rc_api_host_t* host) { rc_api_url_builder_t builder; - rc_api_url_build_dorequest_url(request); + rc_api_url_build_dorequest_url(request, host); rc_url_builder_init(&builder, &request->buffer, 48); if (rc_api_url_build_dorequest(&builder, "unlocks", api_params->username, api_params->api_token)) { @@ -252,3 +280,204 @@ int rc_api_process_fetch_user_unlocks_server_response(rc_api_fetch_user_unlocks_ void rc_api_destroy_fetch_user_unlocks_response(rc_api_fetch_user_unlocks_response_t* response) { rc_buffer_destroy(&response->response.buffer); } + +/* --- Fetch Followed Users --- */ + +int rc_api_init_fetch_followed_users_request(rc_api_request_t* request, const rc_api_fetch_followed_users_request_t* api_params) { + return rc_api_init_fetch_followed_users_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_followed_users_request_hosted(rc_api_request_t* request, + const rc_api_fetch_followed_users_request_t* api_params, + const rc_api_host_t* host) { + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "getfriendlist", api_params->username, api_params->api_token)) { + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + } + + return builder.result; +} + +int rc_api_process_fetch_followed_users_server_response(rc_api_fetch_followed_users_response_t* response, const rc_api_server_response_t* server_response) { + rc_json_field_t array_field; + rc_json_iterator_t iterator; + rc_api_followed_user_t* user; + uint32_t timet; + int result; + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Friends") + }; + + rc_json_field_t followed_user_entry_fields[] = { + RC_JSON_NEW_FIELD("Friend"), + RC_JSON_NEW_FIELD("AvatarUrl"), + RC_JSON_NEW_FIELD("RAPoints"), + RC_JSON_NEW_FIELD("LastSeen"), + RC_JSON_NEW_FIELD("LastSeenTime"), + RC_JSON_NEW_FIELD("LastGameId"), + RC_JSON_NEW_FIELD("LastGameTitle"), + RC_JSON_NEW_FIELD("LastGameIconUrl"), + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!rc_json_get_required_array(&response->num_users, &array_field, &response->response, &fields[2], "Friends")) + return RC_MISSING_VALUE; + + if (response->num_users) { + response->users = (rc_api_followed_user_t*)rc_buffer_alloc(&response->response.buffer, response->num_users * sizeof(rc_api_followed_user_t)); + if (!response->users) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + user = response->users; + while (rc_json_get_array_entry_object(followed_user_entry_fields, sizeof(followed_user_entry_fields) / sizeof(followed_user_entry_fields[0]), &iterator)) { + if (!rc_json_get_required_string(&user->display_name, &response->response, &followed_user_entry_fields[0], "Friend")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&user->avatar_url, &response->response, &followed_user_entry_fields[1], "AvatarUrl")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&user->score, &response->response, &followed_user_entry_fields[2], "RAPoints")) + return RC_MISSING_VALUE; + + rc_json_get_optional_string(&user->recent_activity.description, &response->response, &followed_user_entry_fields[3], "LastSeen", NULL); + + rc_json_get_optional_unum(&timet, &followed_user_entry_fields[4], "LastSeenTime", 0); + user->recent_activity.when = (time_t)timet; + + rc_json_get_optional_unum(&user->recent_activity.context_id, &followed_user_entry_fields[5], "LastGameId", 0); + rc_json_get_optional_string(&user->recent_activity.context, &response->response, &followed_user_entry_fields[6], "LastGameTitle", NULL); + rc_json_get_optional_string(&user->recent_activity.context_image_url, &response->response, &followed_user_entry_fields[7], "LastGameIconUrl", NULL); + + ++user; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_followed_users_response(rc_api_fetch_followed_users_response_t* response) { + rc_buffer_destroy(&response->response.buffer); +} + +/* --- Fetch All Progress --- */ + +int rc_api_init_fetch_all_user_progress_request(rc_api_request_t* request, + const rc_api_fetch_all_user_progress_request_t* api_params) +{ + return rc_api_init_fetch_all_user_progress_request_hosted(request, api_params, &g_host); +} + +int rc_api_init_fetch_all_user_progress_request_hosted(rc_api_request_t* request, + const rc_api_fetch_all_user_progress_request_t* api_params, + const rc_api_host_t* host) +{ + rc_api_url_builder_t builder; + + rc_api_url_build_dorequest_url(request, host); + + if (api_params->console_id == RC_CONSOLE_UNKNOWN) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + if (rc_api_url_build_dorequest(&builder, "allprogress", api_params->username, api_params->api_token)) + { + 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_all_user_progress_server_response(rc_api_fetch_all_user_progress_response_t* response, + const rc_api_server_response_t* server_response) +{ + rc_api_all_user_progress_entry_t* entry; + int result; + + rc_json_field_t fields[] = { + RC_JSON_NEW_FIELD("Success"), + RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Response"), + }; + + rc_json_field_t entry_fields[] = { + RC_JSON_NEW_FIELD("Achievements"), + RC_JSON_NEW_FIELD("Unlocked"), + RC_JSON_NEW_FIELD("UnlockedHardcore"), + }; + + memset(response, 0, sizeof(*response)); + rc_buffer_init(&response->response.buffer); + + result = + rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); + if (result != RC_OK || !response->response.succeeded) + return result; + + if (!fields[2].value_start) { + /* call rc_json_get_required_object to generate the error message */ + rc_json_get_required_object(NULL, 0, &response->response, &fields[2], "Response"); + return RC_MISSING_VALUE; + } + + response->num_entries = fields[2].array_size; + + if (response->num_entries > 0) { + rc_json_iterator_t iterator; + rc_json_field_t field; + char* end; + + rc_buffer_reserve(&response->response.buffer, response->num_entries * sizeof(rc_api_all_user_progress_entry_t)); + + response->entries = (rc_api_all_user_progress_entry_t*)rc_buffer_alloc( + &response->response.buffer, response->num_entries * sizeof(rc_api_all_user_progress_entry_t)); + if (!response->entries) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = fields[2].value_start; + iterator.end = fields[2].value_end; + + entry = response->entries; + while (rc_json_get_next_object_field(&iterator, &field)) + { + entry->game_id = strtol(field.name, &end, 10); + + field.name = ""; + if (!rc_json_get_required_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), + &response->response, &field, "")) + { + return RC_MISSING_VALUE; + } + + rc_json_get_optional_unum(&entry->num_achievements, &entry_fields[0], "Achievements", 0); + rc_json_get_optional_unum(&entry->num_unlocked_achievements, &entry_fields[1], "Unlocked", 0); + rc_json_get_optional_unum(&entry->num_unlocked_achievements_hardcore, &entry_fields[2], "UnlockedHardcore", 0); + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_all_user_progress_response(rc_api_fetch_all_user_progress_response_t* response) +{ + rc_buffer_destroy(&response->response.buffer); +} diff --git a/deps/rcheevos/src/rc_client.c b/deps/rcheevos/src/rc_client.c index 822a59b0f4..1cea7c9515 100644 --- a/deps/rcheevos/src/rc_client.c +++ b/deps/rcheevos/src/rc_client.c @@ -64,6 +64,7 @@ typedef struct rc_client_load_state_t #ifdef RC_CLIENT_SUPPORTS_HASH rc_hash_iterator_t hash_iterator; + rc_client_game_hash_t* tried_hashes[4]; #endif rc_client_pending_media_t* pending_media; @@ -79,7 +80,7 @@ typedef struct rc_client_load_state_t } rc_client_load_state_t; static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state); -static void rc_client_begin_fetch_game_data(rc_client_load_state_t* callback_data); +static void rc_client_begin_fetch_game_sets(rc_client_load_state_t* callback_data); static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game); static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message); static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, const char* hash, const char* file_path); @@ -91,6 +92,70 @@ static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_ static int rc_client_is_award_achievement_pending(const rc_client_t* client, uint32_t achievement_id); static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); +/* ===== natvis extensions ===== */ + +typedef struct __rc_client_achievement_state_enum_t { uint8_t value; } __rc_client_achievement_state_enum_t; +typedef struct __rc_client_achievement_category_enum_t { uint8_t value; } __rc_client_achievement_category_enum_t; +typedef struct __rc_client_achievement_type_enum_t { uint8_t value; } __rc_client_achievement_type_enum_t; +typedef struct __rc_client_achievement_bucket_enum_t { uint8_t value; } __rc_client_achievement_bucket_enum_t; +typedef struct __rc_client_achievement_unlocked_enum_t { uint8_t value; } __rc_client_achievement_unlocked_enum_t; +typedef struct __rc_client_leaderboard_state_enum_t { uint8_t value; } __rc_client_leaderboard_state_enum_t; +typedef struct __rc_client_leaderboard_format_enum_t { uint8_t value; } __rc_client_leaderboard_format_enum_t; +typedef struct __rc_client_log_level_enum_t { uint8_t value; } __rc_client_log_level_enum_t; +typedef struct __rc_client_event_type_enum_t { uint8_t value; } __rc_client_event_type_enum_t; +typedef struct __rc_client_load_game_state_enum_t { uint8_t value; } __rc_client_load_game_state_enum_t; +typedef struct __rc_client_user_state_enum_t { uint8_t value; } __rc_client_user_state_enum_t; +typedef struct __rc_client_mastery_state_enum_t { uint8_t value; } __rc_client_mastery_state_enum_t; +typedef struct __rc_client_spectator_mode_enum_t { uint8_t value; } __rc_client_spectator_mode_enum_t; +typedef struct __rc_client_disconnect_enum_t { uint8_t value; } __rc_client_disconnect_enum_t; +typedef struct __rc_client_leaderboard_tracker_list_t { rc_client_leaderboard_tracker_info_t* first; } __rc_client_leaderboard_tracker_list_t; +typedef struct __rc_client_subset_info_list_t { rc_client_subset_info_t* first; } __rc_client_subset_info_list_t; +typedef struct __rc_client_media_hash_list_t { rc_client_media_hash_t* first; } __rc_client_media_hash_list_t; +typedef struct __rc_client_subset_info_achievements_list_t { rc_client_subset_info_t info; } __rc_client_subset_info_achievements_list_t; +typedef struct __rc_client_subset_info_leaderboards_list_t { rc_client_subset_info_t info; } __rc_client_subset_info_leaderboards_list_t; +typedef struct __rc_client_scheduled_callback_list_t { rc_client_state_t state; } __rc_client_scheduled_callback_list_t; +typedef struct __rc_client_game_hash_list_t { rc_client_t client; } __rc_client_game_hash_list_t; + +static void rc_client_natvis_helper(const rc_client_event_t* event, rc_client_t* client) +{ + struct natvis_extensions { + __rc_client_achievement_state_enum_t achievement_state; + __rc_client_achievement_category_enum_t achievement_category; + __rc_client_achievement_type_enum_t achievement_type; + __rc_client_achievement_bucket_enum_t achievement_bucket; + __rc_client_achievement_unlocked_enum_t achievement_unlocked; + __rc_client_leaderboard_state_enum_t leaderboard_state; + __rc_client_leaderboard_format_enum_t leaderboard_format; + __rc_client_log_level_enum_t log_level; + __rc_client_event_type_enum_t event_type; + __rc_client_load_game_state_enum_t load_game_state; + __rc_client_user_state_enum_t user_state; + __rc_client_mastery_state_enum_t mastery_state; + __rc_client_spectator_mode_enum_t spectator_mode; + __rc_client_disconnect_enum_t disconnect; + __rc_client_leaderboard_tracker_list_t leaderboard_tracker_list; + __rc_client_subset_info_list_t subset_info_list; + __rc_client_media_hash_list_t media_hash_list; + __rc_client_subset_info_achievements_list_t subset_info_achievements_list; + __rc_client_subset_info_leaderboards_list_t subset_info_leaderboards_list; + __rc_client_scheduled_callback_list_t scheduled_callback_list; + __rc_client_game_hash_list_t client_game_hash_list; + } natvis; + + memset(&natvis, 0, sizeof(natvis)); + (void)event; + (void)client; + + /* this code should never be executed. it just ensures these constants get defined for + * the natvis VisualStudio extension as they're not used directly in the code. */ + natvis.achievement_type.value = RC_CLIENT_ACHIEVEMENT_TYPE_STANDARD; + natvis.achievement_type.value = RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE; + natvis.achievement_type.value = RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION; + natvis.achievement_type.value = RC_CLIENT_ACHIEVEMENT_TYPE_WIN; + natvis.achievement_category.value = RC_CLIENT_ACHIEVEMENT_CATEGORY_NONE; + natvis.event_type.value = RC_CLIENT_EVENT_TYPE_NONE; +} + /* ===== Construction/Destruction ===== */ static void rc_client_dummy_event_handler(const rc_client_event_t* event, rc_client_t* client) @@ -107,9 +172,11 @@ rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, client->state.hardcore = 1; client->state.required_unpaused_frames = RC_MINIMUM_UNPAUSED_FRAMES; + client->state.allow_background_memory_reads = 1; client->callbacks.read_memory = read_memory_function; client->callbacks.server_call = server_call_function; + client->callbacks.event_handler = rc_client_natvis_helper; client->callbacks.event_handler = rc_client_dummy_event_handler; rc_client_set_legacy_peek(client, RC_CLIENT_LEGACY_PEEK_AUTO); rc_client_set_get_time_millisecs_function(client, NULL); @@ -161,14 +228,6 @@ void rc_client_destroy(rc_client_t* client) /* ===== Logging ===== */ -static rc_client_t* g_hash_client = NULL; - -#ifdef RC_CLIENT_SUPPORTS_HASH -static void rc_client_log_hash_message(const char* message) { - rc_client_log_message(g_hash_client, message); -} -#endif - void rc_client_log_message(const rc_client_t* client, const char* message) { if (client->callbacks.log_call) @@ -180,7 +239,7 @@ static void rc_client_log_message_va(const rc_client_t* client, const char* form if (client->callbacks.log_call) { char buffer[2048]; -#ifdef __STDC_WANT_SECURE_LIB__ +#ifdef __STDC_SECURE_LIB__ vsprintf_s(buffer, sizeof(buffer), format, args); #elif __STDC_VERSION__ >= 199901L /* vsnprintf requires c99 */ vsnprintf(buffer, sizeof(buffer), format, args); @@ -576,7 +635,7 @@ static int rc_client_get_image_url(char buffer[], size_t buffer_size, int image_ 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); + result = rc_api_init_fetch_image_request_hosted(&request, &image_request, NULL); if (result == RC_OK) snprintf(buffer, buffer_size, "%s", request.url); @@ -627,7 +686,7 @@ static void rc_client_login_callback(const rc_api_server_response_t* server_resp login_callback_data->callback(result, error_message, client, login_callback_data->callback_userdata); if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN) - rc_client_begin_fetch_game_data(load_state); + rc_client_begin_fetch_game_sets(load_state); } else { client->user.username = rc_buffer_strcpy(&client->state.buffer, login_response.username); @@ -637,6 +696,7 @@ static void rc_client_login_callback(const rc_api_server_response_t* server_resp else client->user.display_name = rc_buffer_strcpy(&client->state.buffer, login_response.display_name); + client->user.avatar_url = rc_buffer_strcpy(&client->state.buffer, login_response.avatar_url); client->user.token = rc_buffer_strcpy(&client->state.buffer, login_response.api_token); client->user.score = login_response.score; client->user.score_softcore = login_response.score_softcore; @@ -650,7 +710,7 @@ static void rc_client_login_callback(const rc_api_server_response_t* server_resp RC_CLIENT_LOG_INFO_FORMATTED(client, "%s logged in successfully", login_response.display_name); if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN) - rc_client_begin_fetch_game_data(load_state); + rc_client_begin_fetch_game_sets(load_state); if (login_callback_data->callback) login_callback_data->callback(RC_OK, NULL, client, login_callback_data->callback_userdata); @@ -665,7 +725,7 @@ static rc_client_async_handle_t* rc_client_begin_login(rc_client_t* client, { rc_client_generic_callback_data_t* callback_data; rc_api_request_t request; - int result = rc_api_init_login_request(&request, login_request); + int result = rc_api_init_login_request_hosted(&request, login_request, &client->state.host); const char* error_message = rc_error_str(result); if (result == RC_OK) { @@ -820,8 +880,13 @@ const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client) return NULL; #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->get_user_info) - return client->state.external_client->get_user_info(); + if (client->state.external_client) { + if (client->state.external_client->get_user_info_v3) + return client->state.external_client->get_user_info_v3(); + + if (client->state.external_client->get_user_info) + return rc_client_external_convert_v1_user(client, client->state.external_client->get_user_info()); + } #endif return (client->state.user == RC_CLIENT_USER_STATE_LOGGED_IN) ? &client->user : NULL; @@ -832,6 +897,11 @@ int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], si if (!user) return RC_INVALID_STATE; + if (user->avatar_url) { + snprintf(buffer, buffer_size, "%s", user->avatar_url); + return RC_OK; + } + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, user->display_name); } @@ -895,6 +965,126 @@ void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_g rc_mutex_unlock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ } +typedef struct rc_client_fetch_all_user_progress_callback_data_t { + rc_client_t* client; + rc_client_fetch_all_user_progress_callback_t callback; + void* callback_userdata; + uint32_t console_id; + rc_client_async_handle_t async_handle; +} rc_client_fetch_all_user_progress_callback_data_t; + +static void rc_client_fetch_all_user_progress_callback(const rc_api_server_response_t* server_response, + void* callback_data) +{ + rc_client_fetch_all_user_progress_callback_data_t* ap_callback_data = + (rc_client_fetch_all_user_progress_callback_data_t*)callback_data; + rc_client_t* client = ap_callback_data->client; + rc_api_fetch_all_user_progress_response_t ap_response; + const char* error_message; + int result; + + result = rc_client_end_async(client, &ap_callback_data->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) + RC_CLIENT_LOG_VERBOSE(client, "Fetch all progress aborted"); + + free(ap_callback_data); + return; + } + + result = rc_api_process_fetch_all_user_progress_server_response(&ap_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &ap_response.response); + if (error_message) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch all progress for console %u failed: %s", ap_callback_data->console_id, + error_message); + ap_callback_data->callback(result, error_message, NULL, client, ap_callback_data->callback_userdata); + } else { + rc_client_all_user_progress_t* list; + const size_t list_size = sizeof(*list) + sizeof(rc_client_all_user_progress_entry_t) * ap_response.num_entries; + + list = (rc_client_all_user_progress_t*)malloc(list_size); + if (!list) { + ap_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, + ap_callback_data->callback_userdata); + } else { + rc_client_all_user_progress_entry_t* entry = list->entries = + (rc_client_all_user_progress_entry_t*)((uint8_t*)list + sizeof(*list)); + const rc_api_all_user_progress_entry_t* hlentry = ap_response.entries; + const rc_api_all_user_progress_entry_t* stop = hlentry + ap_response.num_entries; + + for (; hlentry < stop; ++hlentry, ++entry) + { + entry->game_id = hlentry->game_id; + entry->num_achievements = hlentry->num_achievements; + entry->num_unlocked_achievements = hlentry->num_unlocked_achievements; + entry->num_unlocked_achievements_hardcore = hlentry->num_unlocked_achievements_hardcore; + } + + list->num_entries = ap_response.num_entries; + + ap_callback_data->callback(RC_OK, NULL, list, client, ap_callback_data->callback_userdata); + } + } + + rc_api_destroy_fetch_all_user_progress_response(&ap_response); + free(ap_callback_data); +} + +rc_client_async_handle_t* rc_client_begin_fetch_all_user_progress(rc_client_t* client, uint32_t console_id, + rc_client_fetch_all_user_progress_callback_t callback, + void* callback_userdata) +{ + rc_api_fetch_all_user_progress_request_t api_params; + rc_client_fetch_all_user_progress_callback_data_t* callback_data; + rc_client_async_handle_t* async_handle; + rc_api_request_t request; + int result; + const char* error_message; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", NULL, client, callback_userdata); + return NULL; + } else if (client->state.user != RC_CLIENT_USER_STATE_LOGGED_IN) { + callback(RC_INVALID_STATE, "client must be logged in", NULL, client, callback_userdata); + return NULL; + } + + api_params.username = client->user.username; + api_params.api_token = client->user.token; + api_params.console_id = console_id; + + result = rc_api_init_fetch_all_user_progress_request_hosted(&request, &api_params, &client->state.host); + + if (result != RC_OK) { + error_message = rc_error_str(result); + callback(result, error_message, NULL, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_fetch_all_user_progress_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->console_id = console_id; + + async_handle = &callback_data->async_handle; + rc_client_begin_async(client, async_handle); + client->callbacks.server_call(&request, rc_client_fetch_all_user_progress_callback, callback_data, client); + rc_api_destroy_request(&request); + + return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; +} + +void rc_client_destroy_all_user_progress(rc_client_all_user_progress_t* list) +{ + free(list); +} + /* ===== Game ===== */ static void rc_client_free_game(rc_client_game_info_t* game) @@ -916,6 +1106,10 @@ static void rc_client_free_load_state(rc_client_load_state_t* load_state) free(load_state->start_session_response); } +#ifdef RC_CLIENT_SUPPORTS_HASH + rc_hash_destroy_iterator(&load_state->hash_iterator); +#endif + free(load_state); } @@ -1065,27 +1259,23 @@ static void rc_client_validate_addresses(rc_client_game_info_t* game, rc_client_ 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++; + rc_memref_list_t* memref_list = &game->runtime.memrefs->memrefs; + for (; memref_list; memref_list = memref_list->next) { + rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_end = memref + memref_list->count; + total_count += memref_list->count; + for (; memref < memref_end; ++memref) { 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; + client->callbacks.read_memory(memref->address, buffer, 1, client) == 0) { + memref->value.type = RC_VALUE_TYPE_NONE; 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; @@ -1413,6 +1603,9 @@ static void rc_client_apply_unlocks(rc_client_subset_info_t* subset, rc_api_unlo } } } + + if (subset->next) + rc_client_apply_unlocks(subset->next, unlocks, num_unlocks, mode); } static void rc_client_free_pending_media(rc_client_pending_media_t* pending_media) @@ -1427,6 +1620,11 @@ static void rc_client_free_pending_media(rc_client_pending_media_t* pending_medi free(pending_media); } +/* NOTE: address validation uses the read_memory callback to make sure the client + * will return data for the requested address. As such, this function must + * respect the `client->state.allow_background_memory_reads setting. Use + * rc_client_queue_activate_game to dispatch this function to the do_frame loop/ + */ static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_start_session_response_t *start_session_response) { rc_client_t* client = load_state->client; @@ -1476,11 +1674,11 @@ static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_s * client->state.load->game. since we've detached the load_state, this has to occur after * we've made the game active. */ if (pending_media->hash) { - rc_client_begin_change_media_from_hash(client, pending_media->hash, + rc_client_begin_change_media(client, pending_media->hash, pending_media->callback, pending_media->callback_userdata); } else { #ifdef RC_CLIENT_SUPPORTS_HASH - rc_client_begin_change_media(client, pending_media->file_path, + rc_client_begin_identify_and_change_media(client, pending_media->file_path, pending_media->data, pending_media->data_size, pending_media->callback, pending_media->callback_userdata); #endif @@ -1497,11 +1695,6 @@ static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_s /* if the game is still being loaded, make sure all the required memory addresses are accessible * so we can mark achievements as unsupported before loading them into the runtime. */ if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_ABORTED) { - /* TODO: it is desirable to not do memory reads from a background thread. Some emulators (like Dolphin) don't - * allow it. Dolphin's solution is to use a dummy read function that says all addresses are valid and - * switches to the actual read function after the callback is called. latter invalid reads will - * mark achievements as unsupported. */ - /* ASSERT: client->game must be set before calling this function so the read_memory callback can query the console_id */ rc_client_validate_addresses(load_state->game, client); @@ -1562,6 +1755,31 @@ static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_s rc_client_free_load_state(load_state); } +static void rc_client_dispatch_activate_game(struct rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data->data; + free(callback_data); + + (void)client; + (void)now; + + rc_client_activate_game(load_state, load_state->start_session_response); +} + +static void rc_client_queue_activate_game(rc_client_load_state_t* load_state) +{ + rc_client_scheduled_callback_data_t* scheduled_callback_data = calloc(1, sizeof(rc_client_scheduled_callback_data_t)); + if (!scheduled_callback_data) { + rc_client_load_error(load_state, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY)); + return; + } + + scheduled_callback_data->callback = rc_client_dispatch_activate_game; + scheduled_callback_data->data = load_state; + + rc_client_schedule_callback(load_state->client, scheduled_callback_data); +} + static void rc_client_start_session_callback(const rc_api_server_response_t* server_response, void* callback_data) { rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; @@ -1592,7 +1810,7 @@ static void rc_client_start_session_callback(const rc_api_server_response_t* ser else if (outstanding_requests < 0) { /* previous load state was aborted, load_state was free'd */ } - else if (outstanding_requests == 0) { + else if (outstanding_requests == 0 && load_state->client->state.allow_background_memory_reads) { rc_client_activate_game(load_state, &start_session_response); } else { @@ -1606,6 +1824,13 @@ static void rc_client_start_session_callback(const rc_api_server_response_t* ser /* safer to parse the response again than to try to copy it */ rc_api_process_start_session_response(load_state->start_session_response, server_response->body); } + + if (outstanding_requests == 0) { + if (load_state->client->state.allow_background_memory_reads) + rc_client_activate_game(load_state, load_state->start_session_response); + else + rc_client_queue_activate_game(load_state); + } } rc_api_destroy_start_session_response(&start_session_response); @@ -1625,7 +1850,7 @@ static void rc_client_begin_start_session(rc_client_load_state_t* load_state) start_session_params.game_hash = load_state->hash->hash; start_session_params.hardcore = client->state.hardcore; - result = rc_api_init_start_session_request(&start_session_request, &start_session_params); + result = rc_api_init_start_session_request_hosted(&start_session_request, &start_session_params, &client->state.host); if (result != RC_OK) { rc_client_load_error(load_state, result, rc_error_str(result)); } @@ -1648,9 +1873,10 @@ static void rc_client_copy_achievements(rc_client_load_state_t* load_state, rc_client_achievement_info_t* achievement; rc_client_achievement_info_t* scan; rc_buffer_t* buffer; - rc_parse_state_t parse; + rc_preparse_state_t preparse; const char* memaddr; size_t size; + rc_trigger_t* trigger; int trigger_size; subset->achievements = NULL; @@ -1680,11 +1906,11 @@ static void rc_client_copy_achievements(rc_client_load_state_t* load_state, + 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_buffer_reserve(&load_state->game->buffer, size * num_achievements); + buffer = &load_state->game->buffer; + rc_buffer_reserve(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_buffer_alloc(buffer, size); memset(achievements, 0, size); @@ -1703,11 +1929,18 @@ static void rc_client_copy_achievements(rc_client_load_state_t* load_state, achievement->public_.rarity = read->rarity; achievement->public_.rarity_hardcore = read->rarity_hardcore; achievement->public_.type = read->type; /* assert: mapping is 1:1 */ + achievement->public_.badge_url = rc_buffer_strcpy(buffer, read->badge_url); + achievement->public_.badge_locked_url = rc_buffer_strcpy(buffer, read->badge_locked_url); memaddr = read->definition; rc_runtime_checksum(memaddr, achievement->md5); - trigger_size = rc_trigger_size(memaddr); + rc_init_preparse_state(&preparse); + preparse.parse.existing_memrefs = load_state->game->runtime.memrefs; + trigger = RC_ALLOC(rc_trigger_t, &preparse.parse); + rc_parse_trigger_internal(trigger, &memaddr, &preparse.parse); + + trigger_size = preparse.parse.offset; 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; @@ -1715,23 +1948,22 @@ static void rc_client_copy_achievements(rc_client_load_state_t* load_state, } else { /* populate the item, using the communal memrefs pool */ - rc_init_parse_state(&parse, rc_buffer_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); + rc_reset_parse_state(&preparse.parse, rc_buffer_reserve(buffer, trigger_size)); + rc_preparse_reserve_memrefs(&preparse, load_state->game->runtime.memrefs); + achievement->trigger = RC_ALLOC(rc_trigger_t, &preparse.parse); + memaddr = read->definition; + rc_parse_trigger_internal(achievement->trigger, &memaddr, &preparse.parse); - if (parse.offset < 0) { - RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", parse.offset, read->id); + if (preparse.parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", preparse.parse.offset, read->id); achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED; achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED; } else { - rc_buffer_consume(buffer, parse.buffer, (uint8_t*)parse.buffer + parse.offset); - achievement->trigger->memrefs = NULL; /* memrefs managed by runtime */ + rc_buffer_consume(buffer, preparse.parse.buffer, (uint8_t*)preparse.parse.buffer + preparse.parse.offset); } - rc_destroy_parse_state(&parse); + rc_destroy_preparse_state(&preparse); } achievement->created_time = read->created; @@ -1795,10 +2027,11 @@ static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state, rc_client_leaderboard_info_t* leaderboards; rc_client_leaderboard_info_t* leaderboard; rc_buffer_t* buffer; - rc_parse_state_t parse; + rc_preparse_state_t preparse; const char* memaddr; const char* ptr; size_t size; + rc_lboard_t* lboard; int lboard_size; subset->leaderboards = NULL; @@ -1848,29 +2081,32 @@ static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state, leaderboard->value_djb2 = hash; } - lboard_size = rc_lboard_size(memaddr); + rc_init_preparse_state(&preparse); + preparse.parse.existing_memrefs = load_state->game->runtime.memrefs; + lboard = RC_ALLOC(rc_lboard_t, &preparse.parse); + rc_parse_lboard_internal(lboard, memaddr, &preparse.parse); + + lboard_size = preparse.parse.offset; 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_buffer_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); + rc_reset_parse_state(&preparse.parse, rc_buffer_reserve(buffer, lboard_size)); + rc_preparse_reserve_memrefs(&preparse, load_state->game->runtime.memrefs); + leaderboard->lboard = RC_ALLOC(rc_lboard_t, &preparse.parse); + rc_parse_lboard_internal(leaderboard->lboard, memaddr, &preparse.parse); - if (parse.offset < 0) { - RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", parse.offset, read->id); + if (preparse.parse.offset < 0) { + RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", preparse.parse.offset, read->id); leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED; } else { - rc_buffer_consume(buffer, parse.buffer, (uint8_t*)parse.buffer + parse.offset); - leaderboard->lboard->memrefs = NULL; /* memrefs managed by runtime */ + rc_buffer_consume(buffer, preparse.parse.buffer, (uint8_t*)preparse.parse.buffer + preparse.parse.offset); } - rc_destroy_parse_state(&parse); + rc_destroy_preparse_state(&preparse); } ++leaderboard; @@ -1880,27 +2116,10 @@ static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state, 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_buffer_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) +static void rc_client_fetch_game_sets_callback(const rc_api_server_response_t* server_response, void* callback_data) { rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data; - rc_api_fetch_game_data_response_t fetch_game_data_response; + rc_api_fetch_game_sets_response_t fetch_game_sets_response; int outstanding_requests; const char* error_message; int result; @@ -1917,31 +2136,40 @@ static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* s return; } - result = rc_api_process_fetch_game_data_server_response(&fetch_game_data_response, server_response); - error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_game_data_response.response); + result = rc_api_process_fetch_game_sets_server_response(&fetch_game_sets_response, server_response); + error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_game_sets_response.response); outstanding_requests = rc_client_end_load_state(load_state); - if (error_message) { + if (error_message && result != RC_NOT_FOUND) { rc_client_load_error(load_state, result, error_message); } else if (outstanding_requests < 0) { /* previous load state was aborted, load_state was free'd */ } + else if (fetch_game_sets_response.id == 0) { + load_state->hash->game_id = 0; + rc_client_process_resolved_hash(load_state); + } else { - rc_client_subset_info_t* subset; + rc_client_subset_info_t** next_subset; + rc_client_subset_info_t* first_subset = NULL; + uint32_t set_index; - subset = (rc_client_subset_info_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t)); - memset(subset, 0, sizeof(*subset)); - subset->public_.id = 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; + /* hash exists outside the load state - always update it */ + load_state->hash->game_id = fetch_game_sets_response.id; + RC_CLIENT_LOG_INFO_FORMATTED(load_state->client, "Identified game: %u \"%s\" (%s)", load_state->hash->game_id, fetch_game_sets_response.title, load_state->hash->hash); + + if (load_state->hash->hash[0] != '[') { + /* not [NO HASH] or [SUBSETxx] */ + load_state->game->public_.id = load_state->hash->game_id; + load_state->game->public_.hash = load_state->hash->hash; + } if (load_state->game->public_.console_id != RC_CONSOLE_UNKNOWN && - fetch_game_data_response.console_id != load_state->game->public_.console_id) { + fetch_game_sets_response.console_id != load_state->game->public_.console_id) { RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Data for game %u is for console %u, expecting console %u", - fetch_game_data_response.id, fetch_game_data_response.console_id, load_state->game->public_.console_id); + fetch_game_sets_response.id, fetch_game_sets_response.console_id, load_state->game->public_.console_id); } /* kick off the start session request while we process the game data */ @@ -1955,72 +2183,74 @@ static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* s } /* process the game data */ - rc_client_copy_achievements(load_state, 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); + next_subset = &first_subset; + for (set_index = 0; set_index < fetch_game_sets_response.num_sets; ++set_index) { + rc_api_achievement_set_definition_t* set = &fetch_game_sets_response.sets[set_index]; + rc_client_subset_info_t* subset; + + subset = (rc_client_subset_info_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t)); + memset(subset, 0, sizeof(*subset)); + subset->public_.id = set->id; + subset->active = 1; + snprintf(subset->public_.badge_name, sizeof(subset->public_.badge_name), "%s", set->image_name); + subset->public_.badge_url = rc_buffer_strcpy(&load_state->game->buffer, set->image_url); + subset->public_.title = rc_buffer_strcpy(&load_state->game->buffer, set->title); + + rc_client_copy_achievements(load_state, subset, set->achievements, set->num_achievements); + rc_client_copy_leaderboards(load_state, subset, set->leaderboards, set->num_leaderboards); + + if (set->type == RC_ACHIEVEMENT_SET_TYPE_CORE) { + if (!first_subset) + next_subset = &subset->next; + subset->next = first_subset; + first_subset = subset; + } + else { + *next_subset = subset; + next_subset = &subset->next; + } + } + + if (!first_subset) { + rc_client_load_error(load_state, RC_NOT_FOUND, "Response contained no sets"); + } else { + load_state->subset = first_subset; - if (!load_state->game->subsets) { /* core set */ rc_mutex_lock(&load_state->client->state.mutex); - load_state->game->public_.title = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_data_response.title); - load_state->game->subsets = subset; - load_state->game->public_.badge_name = subset->public_.badge_name; - load_state->game->public_.console_id = fetch_game_data_response.console_id; + load_state->game->public_.title = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_sets_response.title); + load_state->game->subsets = first_subset; + load_state->game->public_.badge_name = first_subset->public_.badge_name; + load_state->game->public_.badge_url = first_subset->public_.badge_url; + load_state->game->public_.console_id = fetch_game_sets_response.console_id; rc_mutex_unlock(&load_state->client->state.mutex); - 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 (fetch_game_sets_response.rich_presence_script && fetch_game_sets_response.rich_presence_script[0]) { + result = rc_runtime_activate_richpresence(&load_state->game->runtime, fetch_game_sets_response.rich_presence_script, NULL, 0); if (result != RC_OK) { RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing rich presence", result); } } - } - 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) { - 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_buffer_strcpy(&load_state->game->buffer, fetch_game_data_response.title); + if (load_state->client->callbacks.post_process_game_sets_response) { + load_state->client->callbacks.post_process_game_sets_response(server_response, + &fetch_game_sets_response, load_state->client, load_state->callback_userdata); } - - /* append to subset list */ - scan = load_state->game->subsets; - while (scan->next) - scan = scan->next; - scan->next = subset; - } - - if (load_state->client->callbacks.post_process_game_data_response) { - load_state->client->callbacks.post_process_game_data_response(server_response, - &fetch_game_data_response, load_state->client, load_state->callback_userdata); } outstanding_requests = rc_client_end_load_state(load_state); if (outstanding_requests < 0) { /* previous load state was aborted, load_state was free'd */ } - else { - if (outstanding_requests == 0) + else if (outstanding_requests == 0) { + if (load_state->client->state.allow_background_memory_reads) rc_client_activate_game(load_state, load_state->start_session_response); + else + rc_client_queue_activate_game(load_state); } } - rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); + rc_api_destroy_fetch_game_sets_response(&fetch_game_sets_response); } static rc_client_game_info_t* rc_client_allocate_game(void) @@ -2102,8 +2332,8 @@ static void rc_client_external_load_state_callback(int result, const char* error } else { /* keep partial game object for media_hash management */ - if (client->state.external_client && client->state.external_client->get_game_info) { - const rc_client_game_t* info = client->state.external_client->get_game_info(); + if (client->state.external_client) { + const rc_client_game_t* info = rc_client_get_game_info(client); load_state->game->public_.console_id = info->console_id; client->game = load_state->game; load_state->game = NULL; @@ -2118,6 +2348,23 @@ static void rc_client_external_load_state_callback(int result, const char* error #endif +static void rc_client_initialize_unknown_game(rc_client_game_info_t* game) +{ + rc_client_subset_info_t* subset; + char buffer[64]; + + subset = (rc_client_subset_info_t*)rc_buffer_alloc(&game->buffer, sizeof(rc_client_subset_info_t)); + memset(subset, 0, sizeof(*subset)); + subset->public_.title = ""; + game->subsets = subset; + + game->public_.title = "Unknown Game"; + game->public_.badge_name = ""; + + rc_client_get_image_url(buffer, sizeof(buffer), RC_IMAGE_TYPE_GAME, "000001"); + game->public_.badge_url = rc_buffer_strcpy(&game->buffer, buffer); +} + static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state) { rc_client_t* client = load_state->client; @@ -2133,40 +2380,31 @@ static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state) return; } - if (load_state->game->media_hash && - load_state->game->media_hash->game_hash && - load_state->game->media_hash->game_hash->next) { + if (load_state->tried_hashes[1]) { /* 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; + size_t i; + size_t count = 0; char* ptr; - size_t size; + size_t size = 0; - size = strlen(game_hash->hash) + 1; - while (game_hash->next) { - game_hash = game_hash->next; - size += strlen(game_hash->hash) + 1; + for (i = 0; i < sizeof(load_state->tried_hashes) / sizeof(load_state->tried_hashes[0]); ++i) { + if (!load_state->tried_hashes[i]) + break; + + size += strlen(load_state->tried_hashes[i]->hash) + 1; count++; } ptr = (char*)rc_buffer_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; + for (i = 0; i < count; i++) { + const size_t hash_len = strlen(load_state->tried_hashes[i]->hash); + memcpy(ptr, load_state->tried_hashes[i]->hash, hash_len); + ptr += hash_len; + *ptr++ = ','; + } + *(ptr - 1) = '\0'; + load_state->game->public_.console_id = RC_CONSOLE_UNKNOWN; } else { /* only a single hash was tried, capture it */ @@ -2178,11 +2416,14 @@ static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state) load_state->hash_console_id, load_state->hash->hash, client, load_state->callback_userdata); if (load_state->hash->game_id != 0) { + load_state->hash->is_unknown = 1; RC_CLIENT_LOG_INFO_FORMATTED(load_state->client, "Client says to load game %u for unidentified hash %s", load_state->hash->game_id, load_state->hash->hash); } } } + + rc_hash_destroy_iterator(&load_state->hash_iterator); /* done with this now */ #else load_state->game->public_.console_id = RC_CONSOLE_UNKNOWN; load_state->game->public_.hash = load_state->hash->hash; @@ -2190,6 +2431,12 @@ static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state) if (load_state->hash->game_id == 0) { #ifdef RC_CLIENT_SUPPORTS_EXTERNAL + #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION + if (client->state.raintegration && client->state.raintegration->set_console_id) { + if (load_state->game->public_.console_id != RC_CONSOLE_UNKNOWN) + client->state.raintegration->set_console_id(load_state->game->public_.console_id); + } + #endif if (client->state.external_client) { if (client->state.external_client->load_unknown_game) { client->state.external_client->load_unknown_game(load_state->game->public_.hash); @@ -2200,16 +2447,8 @@ static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state) } else { #endif - /* mimics rc_client_load_unknown_game without allocating a new game object */ - rc_client_subset_info_t* subset; + rc_client_initialize_unknown_game(load_state->game); - subset = (rc_client_subset_info_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t)); - memset(subset, 0, sizeof(*subset)); - subset->public_.title = ""; - - load_state->game->public_.title = "Unknown Game"; - load_state->game->public_.badge_name = ""; - load_state->game->subsets = subset; client->game = load_state->game; load_state->game = NULL; @@ -2226,9 +2465,6 @@ static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state) load_state->game->public_.hash = load_state->hash->hash; } - /* done with the hashing code, release the global pointer */ - g_hash_client = NULL; - #ifdef RC_CLIENT_SUPPORTS_EXTERNAL if (client->state.external_client) { if (client->state.external_client->add_game_hash) @@ -2242,25 +2478,18 @@ static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state) } #endif - rc_client_begin_fetch_game_data(load_state); + rc_client_begin_fetch_game_sets(load_state); } void rc_client_load_unknown_game(rc_client_t* client, const char* tried_hashes) { - rc_client_subset_info_t* subset; rc_client_game_info_t* game; game = rc_client_allocate_game(); if (!game) return; - subset = (rc_client_subset_info_t*)rc_buffer_alloc(&game->buffer, sizeof(rc_client_subset_info_t)); - memset(subset, 0, sizeof(*subset)); - subset->public_.title = ""; - game->subsets = subset; - - game->public_.title = "Unknown Game"; - game->public_.badge_name = ""; + rc_client_initialize_unknown_game(game); game->public_.console_id = RC_CONSOLE_UNKNOWN; if (strlen(tried_hashes) == 32) { /* only one hash tried, add it to the list */ @@ -2276,9 +2505,9 @@ void rc_client_load_unknown_game(rc_client_t* client, const char* tried_hashes) client->game = game; } -static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) +static void rc_client_begin_fetch_game_sets(rc_client_load_state_t* load_state) { - rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_api_fetch_game_sets_request_t fetch_game_sets_request; rc_client_t* client = load_state->client; rc_api_request_t request; int result; @@ -2287,6 +2516,8 @@ static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) result = client->state.user; if (result == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) load_state->progress = RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN; + else + load_state->progress = RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA; rc_mutex_unlock(&client->state.mutex); switch (result) { @@ -2302,26 +2533,31 @@ static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) return; } - memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request)); - fetch_game_data_request.username = client->user.username; - fetch_game_data_request.api_token = client->user.token; - fetch_game_data_request.game_id = load_state->hash->game_id; + memset(&fetch_game_sets_request, 0, sizeof(fetch_game_sets_request)); + fetch_game_sets_request.username = client->user.username; + fetch_game_sets_request.api_token = client->user.token; - result = rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request); + if (load_state->hash->is_unknown) /* lookup failed, but client provided a mapping */ + fetch_game_sets_request.game_id = load_state->hash->game_id; + else + fetch_game_sets_request.game_hash = load_state->hash->hash; + + result = rc_api_init_fetch_game_sets_request_hosted(&request, &fetch_game_sets_request, &client->state.host); if (result != RC_OK) { rc_client_load_error(load_state, result, rc_error_str(result)); return; } - rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA, 1); + rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME, 1); + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Fetching data for hash %s", fetch_game_sets_request.game_hash); - RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Fetching data for game %u", fetch_game_data_request.game_id); rc_client_begin_async(client, &load_state->async_handle); - client->callbacks.server_call(&request, rc_client_fetch_game_data_callback, load_state, client); + client->callbacks.server_call(&request, rc_client_fetch_game_sets_callback, load_state, client); rc_api_destroy_request(&request); } +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL 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; @@ -2366,6 +2602,7 @@ static void rc_client_identify_game_callback(const rc_api_server_response_t* ser rc_api_destroy_resolve_hash_response(&resolve_hash_response); } +#endif rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash) { @@ -2410,6 +2647,9 @@ static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* loa { rc_client_t* client = load_state->client; rc_client_game_hash_t* old_hash; +#ifdef RC_CLIENT_SUPPORTS_HASH + size_t i; +#endif if (!rc_client_attach_load_state(client, load_state)) { rc_client_free_load_state(load_state); @@ -2419,6 +2659,24 @@ static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* loa old_hash = load_state->hash; load_state->hash = rc_client_find_game_hash(client, hash); +#ifdef RC_CLIENT_SUPPORTS_HASH + i = 0; + do { + if (!load_state->tried_hashes[i]) { + load_state->tried_hashes[i] = load_state->hash; + break; + } + + if (load_state->tried_hashes[i] == load_state->hash) + break; + + if (++i == sizeof(load_state->tried_hashes) / sizeof(load_state->tried_hashes[0])) { + RC_CLIENT_LOG_VERBOSE(client, "tried_hashes buffer is full"); + break; + } + } while (1); +#endif + if (file_path) { rc_client_media_hash_t* media_hash = (rc_client_media_hash_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(*media_hash)); @@ -2431,7 +2689,14 @@ static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* loa load_state->game->media_hash->game_hash = load_state->hash; } - if (load_state->hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) { + if (load_state->hash->game_id == 0) { + rc_client_process_resolved_hash(load_state); + } +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + else if (load_state->hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID && + client->state.external_client && client->state.external_client->add_game_hash) { + /* if an add_game_hash external handler exists, do the identification locally, then + * pass the resulting game_id/hash to the external client */ rc_api_resolve_hash_request_t resolve_hash_request; rc_api_request_t request; int result; @@ -2439,7 +2704,7 @@ static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* loa 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); + result = rc_api_init_resolve_hash_request_hosted(&request, &resolve_hash_request, &client->state.host); if (result != RC_OK) { rc_client_load_error(load_state, result, rc_error_str(result)); return NULL; @@ -2452,15 +2717,39 @@ static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* loa rc_api_destroy_request(&request); } + else if (load_state->hash->game_id != RC_CLIENT_UNKNOWN_GAME_ID && + client->state.external_client && client->state.external_client->begin_load_game) { + rc_client_begin_async(client, &load_state->async_handle); + client->state.external_client->begin_load_game(client, load_state->hash->hash, rc_client_external_load_state_callback, load_state); + } +#endif else { - RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); - - rc_client_process_resolved_hash(load_state); + rc_client_begin_fetch_game_sets(load_state); } return (client->state.load == load_state) ? &load_state->async_handle : NULL; } +static void rc_client_abort_load_in_progress(rc_client_t* client) +{ + rc_client_load_state_t* load_state; + + rc_mutex_lock(&client->state.mutex); + + load_state = client->state.load; + if (load_state) { + /* this mimics rc_client_abort_async without nesting the lock */ + load_state->async_handle.aborted = RC_CLIENT_ASYNC_ABORTED; + + client->state.load = NULL; + } + + rc_mutex_unlock(&client->state.mutex); + + if (load_state && load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", load_state->client, load_state->callback_userdata); +} + 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; @@ -2475,6 +2764,8 @@ rc_client_async_handle_t* rc_client_begin_load_game(rc_client_t* client, const c return NULL; } + rc_client_abort_load_in_progress(client); + #ifdef RC_CLIENT_SUPPORTS_EXTERNAL if (client->state.external_client && client->state.external_client->begin_load_game) return client->state.external_client->begin_load_game(client, hash, callback, callback_userdata); @@ -2503,6 +2794,32 @@ rc_hash_iterator_t* rc_client_get_load_state_hash_iterator(rc_client_t* client) return NULL; } +static void rc_client_log_hash_message_verbose(const char* message, const rc_hash_iterator_t* iterator) +{ + rc_client_load_state_t unused; + rc_client_load_state_t* load_state = (rc_client_load_state_t*)(((uint8_t*)iterator) - RC_OFFSETOF(unused, hash_iterator)); + if (load_state->client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) + rc_client_log_message(load_state->client, message); +} + +static void rc_client_log_hash_message_error(const char* message, const rc_hash_iterator_t* iterator) +{ + rc_client_load_state_t unused; + rc_client_load_state_t* load_state = (rc_client_load_state_t*)(((uint8_t*)iterator) - RC_OFFSETOF(unused, hash_iterator)); + if (load_state->client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) + rc_client_log_message(load_state->client, message); +} + +void rc_client_set_hash_callbacks(rc_client_t* client, const struct rc_hash_callbacks* callbacks) +{ + memcpy(&client->callbacks.hash, callbacks, sizeof(*callbacks)); + + if (!callbacks->verbose_message) + client->callbacks.hash.verbose_message = rc_client_log_hash_message_verbose; + if (!callbacks->error_message) + client->callbacks.hash.error_message = rc_client_log_hash_message_error; +} + rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* client, uint32_t console_id, const char* file_path, const uint8_t* data, size_t data_size, @@ -2516,6 +2833,8 @@ rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* cl return NULL; } + rc_client_abort_load_in_progress(client); + #ifdef RC_CLIENT_SUPPORTS_EXTERNAL /* if a add_game_hash handler exists, do the identification locally, then pass the * resulting game_id/hash to the external client */ @@ -2541,12 +2860,6 @@ rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* cl return NULL; } - if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { - g_hash_client = client; - rc_hash_init_error_message_callback(rc_client_log_hash_message); - rc_hash_init_verbose_message_callback(rc_client_log_hash_message); - } - if (!file_path) file_path = "?"; @@ -2559,9 +2872,17 @@ rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* cl load_state->callback = callback; load_state->callback_userdata = callback_userdata; - if (console_id == RC_CONSOLE_UNKNOWN) { - rc_hash_initialize_iterator(&load_state->hash_iterator, file_path, data, data_size); + /* initialize the iterator */ + rc_hash_initialize_iterator(&load_state->hash_iterator, file_path, data, data_size); + rc_hash_merge_callbacks(&load_state->hash_iterator, &client->callbacks.hash); + if (!load_state->hash_iterator.callbacks.verbose_message) + load_state->hash_iterator.callbacks.verbose_message = rc_client_log_hash_message_verbose; + if (!load_state->hash_iterator.callbacks.error_message) + load_state->hash_iterator.callbacks.error_message = rc_client_log_hash_message_error; + + /* calculate the hash */ + if (console_id == RC_CONSOLE_UNKNOWN) { if (!rc_hash_iterate(hash, &load_state->hash_iterator)) { rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); return NULL; @@ -2573,17 +2894,12 @@ rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* cl /* ASSERT: hash_iterator->index and hash_iterator->consoles[0] will be 0 from calloc */ load_state->hash_console_id = console_id; - if (data != NULL) { - if (!rc_hash_generate_from_buffer(hash, console_id, data, data_size)) { - rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); - return NULL; - } - } - else { - if (!rc_hash_generate_from_file(hash, console_id, file_path)) { - rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); - return NULL; - } + /* prevent initializing the iterator so it won't try other consoles in rc_client_process_resolved_hash */ + load_state->hash_iterator.index = 0; + + if (!rc_hash_generate(hash, console_id, &load_state->hash_iterator)) { + rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed"); + return NULL; } } @@ -2608,19 +2924,17 @@ int rc_client_get_load_game_state(const rc_client_t* client) int rc_client_is_game_loaded(const rc_client_t* client) { - const rc_client_game_t* game; - if (!client) return 0; #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->get_game_info) - game = client->state.external_client->get_game_info(); - else + if (client->state.external_client) { + const rc_client_game_t* game = rc_client_get_game_info(client); + return (game && game->id != 0); + } #endif - game = client->game ? &client->game->public_ : NULL; - return (game && game->id != 0); + return (client->game && client->game->public_.id != 0); } static void rc_client_game_mark_ui_to_be_hidden(rc_client_t* client, rc_client_game_info_t* game) @@ -2809,7 +3123,7 @@ static rc_client_async_handle_t* rc_client_begin_change_media_internal(rc_client 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); + result = rc_api_init_resolve_hash_request_hosted(&request, &resolve_hash_request, &client->state.host); if (result != RC_OK) { callback(result, rc_error_str(result), client, callback_userdata); return NULL; @@ -2901,7 +3215,7 @@ static rc_client_game_info_t* rc_client_check_pending_media(rc_client_t* client, #ifdef RC_CLIENT_SUPPORTS_HASH -rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path, +rc_client_async_handle_t* rc_client_begin_identify_and_change_media(rc_client_t* client, const char* file_path, const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata) { rc_client_pending_media_t media; @@ -2921,9 +3235,9 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons } #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && !client->state.external_client->begin_change_media_from_hash) { - if (client->state.external_client->begin_change_media) - return client->state.external_client->begin_change_media(client, file_path, data, data_size, callback, callback_userdata); + if (client->state.external_client && !client->state.external_client->begin_change_media) { + if (client->state.external_client->begin_identify_and_change_media) + return client->state.external_client->begin_identify_and_change_media(client, file_path, data, data_size, callback, callback_userdata); } #endif @@ -2953,19 +3267,11 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons char hash[33]; int result; - if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) { - g_hash_client = client; - rc_hash_init_error_message_callback(rc_client_log_hash_message); - rc_hash_init_verbose_message_callback(rc_client_log_hash_message); - } - if (data != NULL) result = rc_hash_generate_from_buffer(hash, game->public_.console_id, data, data_size); else result = rc_hash_generate_from_file(hash, game->public_.console_id, file_path); - g_hash_client = NULL; - if (!result) { /* when changing discs, if the disc is not supported by the system, allow it. this is * primarily for games that support user-provided audio CDs, but does allow using discs @@ -2986,8 +3292,8 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons if (!result) { #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->begin_change_media_from_hash) - return client->state.external_client->begin_change_media_from_hash(client, game_hash->hash, callback, callback_userdata); + if (client->state.external_client && client->state.external_client->begin_change_media) + return client->state.external_client->begin_change_media(client, game_hash->hash, callback, callback_userdata); #endif rc_client_change_media_internal(client, game_hash, callback, callback_userdata); @@ -2999,8 +3305,8 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons if (client->state.external_client) { if (client->state.external_client->add_game_hash) client->state.external_client->add_game_hash(game_hash->hash, game_hash->game_id); - if (client->state.external_client->begin_change_media_from_hash) - return client->state.external_client->begin_change_media_from_hash(client, game_hash->hash, callback, callback_userdata); + if (client->state.external_client->begin_change_media) + return client->state.external_client->begin_change_media(client, game_hash->hash, callback, callback_userdata); } #endif @@ -3009,7 +3315,7 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons #endif /* RC_CLIENT_SUPPORTS_HASH */ -rc_client_async_handle_t* rc_client_begin_change_media_from_hash(rc_client_t* client, const char* hash, +rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* hash, rc_client_callback_t callback, void* callback_userdata) { rc_client_pending_media_t media; @@ -3027,8 +3333,8 @@ rc_client_async_handle_t* rc_client_begin_change_media_from_hash(rc_client_t* cl } #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->begin_change_media_from_hash) { - return client->state.external_client->begin_change_media_from_hash(client, hash, callback, callback_userdata); + if (client->state.external_client && client->state.external_client->begin_change_media) { + return client->state.external_client->begin_change_media(client, hash, callback, callback_userdata); } #endif @@ -3052,8 +3358,13 @@ const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client) return NULL; #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->get_game_info) - return client->state.external_client->get_game_info(); + if (client->state.external_client) { + if (client->state.external_client->get_game_info_v3) + return client->state.external_client->get_game_info_v3(); + + if (client->state.external_client->get_game_info) + return rc_client_external_convert_v1_game(client, client->state.external_client->get_game_info()); + } #endif return client->game ? &client->game->public_ : NULL; @@ -3064,52 +3375,16 @@ int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], si if (!game) return RC_INVALID_STATE; + if (game->badge_url) { + snprintf(buffer, buffer_size, "%s", game->badge_url); + return RC_OK; + } + return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_GAME, game->badge_name); } /* ===== Subsets ===== */ -rc_client_async_handle_t* 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 NULL; - } - -#ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->begin_load_subset) - return client->state.external_client->begin_load_subset(client, subset_id, callback, callback_userdata); -#endif - - if (!rc_client_is_game_loaded(client)) { - callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); - return NULL; - } - - snprintf(buffer, sizeof(buffer), "[SUBSET%lu]", (unsigned long)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 NULL; - } - - 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_process_resolved_hash(load_state); - - return (client->state.load == load_state) ? &load_state->async_handle : NULL; -} - const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id) { rc_client_subset_info_t* subset; @@ -3118,8 +3393,13 @@ const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_ return NULL; #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->get_subset_info) - return client->state.external_client->get_subset_info(subset_id); + if (client->state.external_client) { + if (client->state.external_client->get_subset_info_v3) + return client->state.external_client->get_subset_info_v3(subset_id); + + if (client->state.external_client->get_subset_info) + return rc_client_external_convert_v1_subset(client, client->state.external_client->get_subset_info(subset_id)); + } #endif if (!client->game) @@ -3133,6 +3413,118 @@ const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_ return NULL; } +/* ===== Fetch Game Hashes ===== */ + +typedef struct rc_client_fetch_hash_library_callback_data_t { + rc_client_t* client; + rc_client_fetch_hash_library_callback_t callback; + void* callback_userdata; + uint32_t console_id; + rc_client_async_handle_t async_handle; +} rc_client_fetch_hash_library_callback_data_t; + +static void rc_client_fetch_hash_library_callback(const rc_api_server_response_t* server_response, void* callback_data) +{ + rc_client_fetch_hash_library_callback_data_t* hashlib_callback_data = + (rc_client_fetch_hash_library_callback_data_t*)callback_data; + rc_client_t* client = hashlib_callback_data->client; + rc_api_fetch_hash_library_response_t hashlib_response; + const char* error_message; + int result; + + result = rc_client_end_async(client, &hashlib_callback_data->async_handle); + if (result) { + if (result != RC_CLIENT_ASYNC_DESTROYED) + RC_CLIENT_LOG_VERBOSE(client, "Fetch hash library aborted"); + + free(hashlib_callback_data); + return; + } + + result = rc_api_process_fetch_hash_library_server_response(&hashlib_response, server_response); + error_message = + rc_client_server_error_message(&result, server_response->http_status_code, &hashlib_response.response); + if (error_message) { + RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch hash library for console %u failed: %s", + hashlib_callback_data->console_id, error_message); + hashlib_callback_data->callback(result, error_message, NULL, client, hashlib_callback_data->callback_userdata); + } else { + rc_client_hash_library_t* list; + const size_t list_size = sizeof(*list) + sizeof(rc_client_hash_library_entry_t) * hashlib_response.num_entries; + list = (rc_client_hash_library_t*)malloc(list_size); + if (!list) { + hashlib_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, + hashlib_callback_data->callback_userdata); + } else { + rc_client_hash_library_entry_t* entry = list->entries = + (rc_client_hash_library_entry_t*)((uint8_t*)list + sizeof(*list)); + const rc_api_hash_library_entry_t* hlentry = hashlib_response.entries; + const rc_api_hash_library_entry_t* stop = hlentry + hashlib_response.num_entries; + + for (; hlentry < stop; ++hlentry, ++entry) { + snprintf(entry->hash, sizeof(entry->hash), "%s", hlentry->hash); + entry->game_id = hlentry->game_id; + } + + list->num_entries = hashlib_response.num_entries; + + hashlib_callback_data->callback(RC_OK, NULL, list, client, hashlib_callback_data->callback_userdata); + } + } + + rc_api_destroy_fetch_hash_library_response(&hashlib_response); + free(hashlib_callback_data); +} + +rc_client_async_handle_t* rc_client_begin_fetch_hash_library(rc_client_t* client, uint32_t console_id, + rc_client_fetch_hash_library_callback_t callback, + void* callback_userdata) +{ + rc_api_fetch_hash_library_request_t api_params; + rc_client_fetch_hash_library_callback_data_t* callback_data; + rc_client_async_handle_t* async_handle; + rc_api_request_t request; + int result; + const char* error_message; + + if (!client) { + callback(RC_INVALID_STATE, "client is required", NULL, client, callback_userdata); + return NULL; + } + + api_params.console_id = console_id; + result = rc_api_init_fetch_hash_library_request_hosted(&request, &api_params, &client->state.host); + + if (result != RC_OK) { + error_message = rc_error_str(result); + callback(result, error_message, NULL, client, callback_userdata); + return NULL; + } + + callback_data = (rc_client_fetch_hash_library_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->console_id = console_id; + + async_handle = &callback_data->async_handle; + rc_client_begin_async(client, async_handle); + client->callbacks.server_call(&request, rc_client_fetch_hash_library_callback, callback_data, client); + rc_api_destroy_request(&request); + + return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; +} + +void rc_client_destroy_hash_library(rc_client_hash_library_t* list) +{ + free(list); +} + /* ===== Achievements ===== */ static void rc_client_update_achievement_display_information(rc_client_t* client, rc_client_achievement_info_t* achievement, time_t recent_unlock_time) @@ -3178,8 +3570,11 @@ static void rc_client_update_achievement_display_information(rc_client_t* client 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), - "%lu/%lu", (unsigned long)new_measured_value, (unsigned long)achievement->trigger->measured_target); + char* ptr = achievement->public_.measured_progress; + const int buffer_size = (int)sizeof(achievement->public_.measured_progress); + const int chars = rc_format_value(ptr, buffer_size, (int32_t)new_measured_value, RC_FORMAT_UNSIGNED_VALUE); + ptr[chars] = '/'; + rc_format_value(ptr + chars + 1, buffer_size - chars - 1, (int32_t)achievement->trigger->measured_target, RC_FORMAT_UNSIGNED_VALUE); } else if (achievement->public_.measured_percent >= 1.0) { snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress), @@ -3285,8 +3680,8 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli { rc_client_achievement_info_t* achievement; rc_client_achievement_info_t* stop; - rc_client_achievement_t** bucket_achievements; - rc_client_achievement_t** achievement_ptr; + const rc_client_achievement_t** bucket_achievements; + const rc_client_achievement_t** achievement_ptr; rc_client_achievement_bucket_t* bucket_ptr; rc_client_achievement_list_info_t* list; rc_client_subset_info_t* subset; @@ -3316,8 +3711,14 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli return (rc_client_achievement_list_t*)calloc(1, sizeof(rc_client_achievement_list_info_t)); #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->create_achievement_list) - return (rc_client_achievement_list_t*)client->state.external_client->create_achievement_list(category, grouping); + if (client->state.external_client) { + if (client->state.external_client->create_achievement_list_v3) + return (rc_client_achievement_list_t*)client->state.external_client->create_achievement_list_v3(category, grouping); + + if (client->state.external_client->create_achievement_list) + return rc_client_external_convert_v1_achievement_list(client, + (rc_client_achievement_list_t*)client->state.external_client->create_achievement_list(category, grouping)); + } #endif if (!client->game) @@ -3387,8 +3788,8 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_achievement_bucket_t)); list = (rc_client_achievement_list_info_t*)malloc(list_size + buckets_size + num_achievements * sizeof(rc_client_achievement_t*)); - bucket_ptr = list->public_.buckets = (rc_client_achievement_bucket_t*)((uint8_t*)list + list_size); - achievement_ptr = (rc_client_achievement_t**)((uint8_t*)bucket_ptr + buckets_size); + list->public_.buckets = bucket_ptr = (rc_client_achievement_bucket_t*)((uint8_t*)list + list_size); + achievement_ptr = (const 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) { @@ -3419,9 +3820,9 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli bucket_ptr->bucket_type = bucket_type; if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED) - qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_unlock_times); + qsort((void*)bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_unlock_times); else if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE) - qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_progress); + qsort((void*)bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_progress); ++bucket_ptr; } @@ -3543,8 +3944,13 @@ const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* clien return NULL; #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->get_achievement_info) - return client->state.external_client->get_achievement_info(id); + if (client->state.external_client) { + if (client->state.external_client->get_achievement_info_v3) + return client->state.external_client->get_achievement_info_v3(id); + + if (client->state.external_client->get_achievement_info) + return rc_client_external_convert_v1_achievement(client, client->state.external_client->get_achievement_info(id)); + } #endif if (!client->game) @@ -3567,6 +3973,16 @@ int rc_client_achievement_get_image_url(const rc_client_achievement_t* achieveme if (!achievement || !achievement->badge_name[0]) return rc_client_get_image_url(buffer, buffer_size, image_type, "00000"); + if (image_type == RC_IMAGE_TYPE_ACHIEVEMENT && achievement->badge_url) { + snprintf(buffer, buffer_size, "%s", achievement->badge_url); + return RC_OK; + } + + if (image_type == RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED && achievement->badge_locked_url) { + snprintf(buffer, buffer_size, "%s", achievement->badge_locked_url); + return RC_OK; + } + return rc_client_get_image_url(buffer, buffer_size, image_type, achievement->badge_name); } @@ -3697,8 +4113,7 @@ static void rc_client_award_achievement_callback(const rc_api_server_response_t* 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; + subset->mastery = RC_CLIENT_MASTERY_STATE_PENDING; } } } @@ -3732,7 +4147,7 @@ static void rc_client_award_achievement_server_call(rc_client_award_achievement_ api_params.seconds_since_unlock = (uint32_t)((now - ach_data->unlock_time) / 1000); } - result = rc_api_init_award_achievement_request(&request, &api_params); + result = rc_api_init_award_achievement_request_hosted(&request, &api_params, &ach_data->client->state.host); 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); @@ -3930,8 +4345,8 @@ rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* cli { rc_client_leaderboard_info_t* leaderboard; rc_client_leaderboard_info_t* stop; - rc_client_leaderboard_t** bucket_leaderboards; - rc_client_leaderboard_t** leaderboard_ptr; + const rc_client_leaderboard_t** bucket_leaderboards; + const rc_client_leaderboard_t** leaderboard_ptr; rc_client_leaderboard_bucket_t* bucket_ptr; rc_client_leaderboard_list_info_t* list; rc_client_subset_info_t* subset; @@ -4026,8 +4441,8 @@ rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* cli buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_leaderboard_bucket_t)); list = (rc_client_leaderboard_list_info_t*)malloc(list_size + buckets_size + num_leaderboards * sizeof(rc_client_leaderboard_t*)); - bucket_ptr = list->public_.buckets = (rc_client_leaderboard_bucket_t*)((uint8_t*)list + list_size); - leaderboard_ptr = (rc_client_leaderboard_t**)((uint8_t*)bucket_ptr + buckets_size); + list->public_.buckets = bucket_ptr = (rc_client_leaderboard_bucket_t*)((uint8_t*)list + list_size); + leaderboard_ptr = (const 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) { @@ -4393,7 +4808,7 @@ static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_lead api_params.seconds_since_completion = (uint32_t)((now - lboard_data->submit_time) / 1000); } - result = rc_api_init_submit_lboard_entry_request(&request, &api_params); + result = rc_api_init_submit_lboard_entry_request_hosted(&request, &api_params, &lboard_data->client->state.host); 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; @@ -4567,7 +4982,7 @@ static rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_info(rc_clien int result; const char* error_message; - result = rc_api_init_fetch_leaderboard_info_request(&request, lbinfo_request); + result = rc_api_init_fetch_leaderboard_info_request_hosted(&request, lbinfo_request, &client->state.host); if (result != RC_OK) { error_message = rc_error_str(result); @@ -4693,7 +5108,7 @@ static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, r api_params.game_hash = client->game->public_.hash; api_params.hardcore = client->state.hardcore; - result = rc_api_init_ping_request(&request, &api_params); + result = rc_api_init_ping_request_hosted(&request, &api_params, &client->state.host); if (result != RC_OK) { RC_CLIENT_LOG_WARN_FORMATTED(client, "Error generating ping request: %s", rc_error_str(result)); } @@ -4781,24 +5196,26 @@ void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memo client->callbacks.read_memory = handler; } +void rc_client_set_allow_background_memory_reads(rc_client_t* client, int allowed) +{ + if (!client) + return; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->set_allow_background_memory_reads) + client->state.external_client->set_allow_background_memory_reads(allowed); +#endif + + client->state.allow_background_memory_reads = allowed; +} + static void rc_client_invalidate_processing_memref(rc_client_t* client) { - 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; - } + client->state.processing_memref->value.type = RC_VALUE_TYPE_NONE; rc_client_invalidate_memref_achievements(client->game, client, client->state.processing_memref); rc_client_invalidate_memref_leaderboards(client->game, client, client->state.processing_memref); @@ -4906,31 +5323,57 @@ int rc_client_is_processing_required(rc_client_t* client) 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; - uint32_t value; +static void rc_client_update_memref_values(rc_client_t* client) { + rc_memrefs_t* memrefs = client->game->runtime.memrefs; + rc_memref_list_t* memref_list; + rc_modified_memref_list_t* modified_memref_list; int invalidated_memref = 0; - for (; memref; memref = memref->next) { - if (memref->value.is_indirect) - continue; + memref_list = &memrefs->memrefs; + do { + rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_stop = memref + memref_list->count; + uint32_t value; - client->state.processing_memref = memref; + for (; memref < memref_stop; ++memref) { + if (memref->value.type == RC_VALUE_TYPE_NONE) + continue; - value = rc_peek_value(memref->address, memref->value.size, client->state.legacy_peek, client); + /* if processing_memref is set, and the memory read fails, all dependent achievements will be disabled */ + client->state.processing_memref = memref; - if (client->state.processing_memref) { - rc_update_memref_value(&memref->value, value); + 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; + } } - else { - /* if the peek function cleared the processing_memref, the memref was invalidated */ - invalidated_memref = 1; - } - } + + memref_list = memref_list->next; + } while (memref_list); client->state.processing_memref = NULL; + modified_memref_list = &memrefs->modified_memrefs; + if (modified_memref_list->count) { + do { + rc_modified_memref_t* modified_memref = modified_memref_list->items; + const rc_modified_memref_t* modified_memref_stop = modified_memref + modified_memref_list->count; + + for (; modified_memref < modified_memref_stop; ++modified_memref) + rc_update_memref_value(&modified_memref->memref.value, rc_get_modified_memref_value(modified_memref, client->state.legacy_peek, client)); + + modified_memref_list = modified_memref_list->next; + } while (modified_memref_list); + } + + if (client->game->runtime.richpresence && client->game->runtime.richpresence->richpresence) + rc_update_values(client->game->runtime.richpresence->richpresence->values, client->state.legacy_peek, client); + if (invalidated_memref) rc_client_update_active_achievements(client->game); } @@ -5126,7 +5569,12 @@ static void rc_client_raise_mastery_event(rc_client_t* client, rc_client_subset_ rc_client_event_t client_event; memset(&client_event, 0, sizeof(client_event)); - client_event.type = RC_CLIENT_EVENT_GAME_COMPLETED; + client_event.subset = &subset->public_; + + if (subset == client->game->subsets) + client_event.type = RC_CLIENT_EVENT_GAME_COMPLETED; + else + client_event.type = RC_CLIENT_EVENT_SUBSET_COMPLETED; subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN; @@ -5344,7 +5792,6 @@ void rc_client_do_frame(rc_client_t* client) 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) { @@ -5516,9 +5963,8 @@ static void rc_client_reset_richpresence(rc_client_t* client) 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); + if (client->game->runtime.richpresence && client->game->runtime.richpresence->richpresence) + rc_reset_values(client->game->runtime.richpresence->richpresence->values); } static void rc_client_reset_all(rc_client_t* client) @@ -5571,6 +6017,9 @@ void rc_client_reset(rc_client_t* client) int rc_client_can_pause(rc_client_t* client, uint32_t* frames_remaining) { + if (!client) + return 1; + #ifdef RC_CLIENT_SUPPORTS_EXTERNAL if (client->state.external_client && client->state.external_client->can_pause) return client->state.external_client->can_pause(frames_remaining); @@ -5999,23 +6448,28 @@ 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) +void rc_client_set_host(rc_client_t* client, const char* hostname) { - /* if empty, just pass NULL */ - if (hostname && !hostname[0]) - hostname = NULL; + if (!client) + return; - /* clear the image host so it'll use the custom host for images too */ - rc_api_set_image_host(NULL); + if (client->state.host.host && hostname && strcmp(hostname, client->state.host.host) == 0) + return; - /* set the custom host */ - if (hostname && client) { - RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Using host: %s", hostname); + /* clear out any previously specified host information */ + memset(&client->state.host, 0, sizeof(client->state.host)); + + if (hostname && (!hostname[0] || strcmp(hostname, rc_api_default_host()) == 0)) { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Using host: %s", rc_api_default_host()); + hostname = rc_api_default_host(); + } + else { + RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Using host: %s", hostname); + client->state.host.host = rc_buffer_strcpy(&client->state.buffer, hostname); } - rc_api_set_host(hostname); #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client && client->state.external_client && client->state.external_client->set_host) + if (client->state.external_client && client->state.external_client->set_host) client->state.external_client->set_host(hostname); #endif } diff --git a/deps/rcheevos/src/rc_client_external.c b/deps/rcheevos/src/rc_client_external.c new file mode 100644 index 0000000000..fd8f76cbd0 --- /dev/null +++ b/deps/rcheevos/src/rc_client_external.c @@ -0,0 +1,261 @@ +#include "rc_client_external.h" + +#include "rc_client_external_versions.h" +#include "rc_client_internal.h" + +#include "rc_api_runtime.h" + +#define RC_CONVERSION_FILL(obj, obj_type, src_type) memset((uint8_t*)obj + sizeof(src_type), 0, sizeof(obj_type) - sizeof(src_type)) + +/* https://media.retroachievements.org/Badge/123456_lock.png is 58 with null terminator */ +#define RC_CLIENT_IMAGE_URL_BUFFER_SIZE 64 + +typedef struct rc_client_external_conversions_t { + rc_client_user_t user; + rc_client_game_t game; + rc_client_subset_t subsets[4]; + rc_client_achievement_t achievements[16]; + char user_avatar_url[RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; + char game_badge_url[RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; + char subset_badge_url[4][RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; + char achievement_badge_url[16][RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; + char achievement_badge_locked_url[16][RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; + uint32_t next_subset_index; + uint32_t next_achievement_index; +} rc_client_external_conversions_t; + +static const char* rc_client_external_build_avatar_url(char buffer[], uint32_t image_type, const char* image_name) +{ + rc_api_fetch_image_request_t image_request; + rc_api_request_t request; + int result; + + 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) + return NULL; + + strcpy_s(buffer, RC_CLIENT_IMAGE_URL_BUFFER_SIZE, request.url); + return buffer; +} + +static void rc_client_external_conversions_init(const rc_client_t* client) +{ + if (!client->state.external_client_conversions) { + rc_client_t* mutable_client = (rc_client_t*)client; + rc_client_external_conversions_t* conversions = (rc_client_external_conversions_t*) + rc_buffer_alloc(&mutable_client->state.buffer, sizeof(rc_client_external_conversions_t)); + + memset(conversions, 0, sizeof(*conversions)); + + mutable_client->state.external_client_conversions = conversions; + } +} + +const rc_client_user_t* rc_client_external_convert_v1_user(const rc_client_t* client, const rc_client_user_t* v1_user) +{ + rc_client_user_t* converted; + + if (!v1_user) + return NULL; + + rc_client_external_conversions_init(client); + + converted = &client->state.external_client_conversions->user; + memcpy(converted, v1_user, sizeof(v1_rc_client_user_t)); + RC_CONVERSION_FILL(converted, rc_client_user_t, v1_rc_client_user_t); + + converted->avatar_url = rc_client_external_build_avatar_url( + client->state.external_client_conversions->user_avatar_url, RC_IMAGE_TYPE_USER, v1_user->username); + + return converted; +} + +const rc_client_game_t* rc_client_external_convert_v1_game(const rc_client_t* client, const rc_client_game_t* v1_game) +{ + rc_client_game_t* converted; + + if (!v1_game) + return NULL; + + rc_client_external_conversions_init(client); + + converted = &client->state.external_client_conversions->game; + memcpy(converted, v1_game, sizeof(v1_rc_client_game_t)); + RC_CONVERSION_FILL(converted, rc_client_game_t, v1_rc_client_game_t); + + converted->badge_url = rc_client_external_build_avatar_url( + client->state.external_client_conversions->game_badge_url, RC_IMAGE_TYPE_GAME, v1_game->badge_name); + + return converted; +} + +const rc_client_subset_t* rc_client_external_convert_v1_subset(const rc_client_t* client, const rc_client_subset_t* v1_subset) +{ + rc_client_subset_t* converted = NULL; + char* badge_url = NULL; + const uint32_t num_subsets = sizeof(client->state.external_client_conversions->subsets) / sizeof(client->state.external_client_conversions->subsets[0]); + uint32_t index; + + if (!v1_subset) + return NULL; + + rc_client_external_conversions_init(client); + + for (index = 0; index < num_subsets; ++index) { + if (client->state.external_client_conversions->subsets[index].id == v1_subset->id) { + converted = &client->state.external_client_conversions->subsets[index]; + badge_url = client->state.external_client_conversions->subset_badge_url[index]; + break; + } + } + + if (!converted) { + index = client->state.external_client_conversions->next_subset_index; + converted = &client->state.external_client_conversions->subsets[index]; + badge_url = client->state.external_client_conversions->subset_badge_url[client->state.external_client_conversions->next_subset_index]; + client->state.external_client_conversions->next_subset_index = (index + 1) % num_subsets; + } + + memcpy(converted, v1_subset, sizeof(v1_rc_client_subset_t)); + RC_CONVERSION_FILL(converted, rc_client_subset_t, v1_rc_client_subset_t); + + converted->badge_url = rc_client_external_build_avatar_url(badge_url, RC_IMAGE_TYPE_GAME, v1_subset->badge_name); + + return converted; +} + +const rc_client_achievement_t* rc_client_external_convert_v1_achievement(const rc_client_t* client, const rc_client_achievement_t* v1_achievement) +{ + rc_client_achievement_t* converted = NULL; + char* badge_url = NULL; + char* badge_locked_url = NULL; + const uint32_t num_achievements = sizeof(client->state.external_client_conversions->achievements) / sizeof(client->state.external_client_conversions->achievements[0]); + uint32_t index; + + if (!v1_achievement) + return NULL; + + rc_client_external_conversions_init(client); + + for (index = 0; index < num_achievements; ++index) { + if (client->state.external_client_conversions->achievements[index].id == v1_achievement->id) { + converted = &client->state.external_client_conversions->achievements[index]; + badge_url = client->state.external_client_conversions->achievement_badge_url[index]; + badge_locked_url = client->state.external_client_conversions->achievement_badge_locked_url[index]; + break; + } + } + + if (!converted) { + index = client->state.external_client_conversions->next_achievement_index; + converted = &client->state.external_client_conversions->achievements[index]; + badge_url = client->state.external_client_conversions->achievement_badge_url[index]; + badge_locked_url = client->state.external_client_conversions->achievement_badge_locked_url[index]; + client->state.external_client_conversions->next_achievement_index = (index + 1) % num_achievements; + } + + memcpy(converted, v1_achievement, sizeof(v1_rc_client_achievement_t)); + RC_CONVERSION_FILL(converted, rc_client_achievement_t, v1_rc_client_achievement_t); + + converted->badge_url = rc_client_external_build_avatar_url(badge_url, RC_IMAGE_TYPE_ACHIEVEMENT, v1_achievement->badge_name); + converted->badge_locked_url = rc_client_external_build_avatar_url(badge_locked_url, RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, v1_achievement->badge_name); + + return converted; +} + +typedef struct rc_client_achievement_list_wrapper_t { + rc_client_achievement_list_info_t info; + rc_client_achievement_list_t* source_list; + rc_client_achievement_t* achievements; + rc_client_achievement_t** achievements_pointers; + char* badge_url_buffer; +} rc_client_achievement_list_wrapper_t; + +static void rc_client_destroy_achievement_list_wrapper(rc_client_achievement_list_info_t* info) +{ + rc_client_achievement_list_wrapper_t* wrapper = (rc_client_achievement_list_wrapper_t*)info; + + if (wrapper->achievements) + free(wrapper->achievements); + if (wrapper->achievements_pointers) + free(wrapper->achievements_pointers); + if (wrapper->info.public_.buckets) + free((void*)wrapper->info.public_.buckets); + if (wrapper->badge_url_buffer) + free(wrapper->badge_url_buffer); + + rc_client_destroy_achievement_list(wrapper->source_list); + + free(wrapper); +} + +rc_client_achievement_list_t* rc_client_external_convert_v1_achievement_list(const rc_client_t* client, rc_client_achievement_list_t* v1_achievement_list) +{ + rc_client_achievement_list_wrapper_t* new_list; + (void)client; + + if (!v1_achievement_list) + return NULL; + + new_list = (rc_client_achievement_list_wrapper_t*)calloc(1, sizeof(*new_list)); + if (!new_list) + return NULL; + + new_list->source_list = v1_achievement_list; + new_list->info.destroy_func = rc_client_destroy_achievement_list_wrapper; + + if (v1_achievement_list->num_buckets) { + const v1_rc_client_achievement_bucket_t* src_bucket = (const v1_rc_client_achievement_bucket_t*)&v1_achievement_list->buckets[0]; + const v1_rc_client_achievement_bucket_t* stop_bucket = src_bucket + v1_achievement_list->num_buckets; + rc_client_achievement_bucket_t* bucket; + uint32_t num_achievements = 0; + char* badge_url = NULL; + + new_list->info.public_.buckets = bucket = (rc_client_achievement_bucket_t*)calloc(v1_achievement_list->num_buckets, sizeof(*new_list->info.public_.buckets)); + if (!new_list->info.public_.buckets) + return (rc_client_achievement_list_t*)new_list; + + for (; src_bucket < stop_bucket; src_bucket++) + num_achievements += src_bucket->num_achievements; + + if (num_achievements) { + new_list->achievements = (rc_client_achievement_t*)calloc(num_achievements, sizeof(*new_list->achievements)); + new_list->achievements_pointers = (rc_client_achievement_t**)malloc(num_achievements * sizeof(rc_client_achievement_t*)); + new_list->badge_url_buffer = badge_url = (char*)malloc(num_achievements * 2 * RC_CLIENT_IMAGE_URL_BUFFER_SIZE); + if (!new_list->achievements || !new_list->achievements_pointers || !new_list->badge_url_buffer) + return (rc_client_achievement_list_t*)new_list; + } + + num_achievements = 0; + src_bucket = (const v1_rc_client_achievement_bucket_t*)&v1_achievement_list->buckets[0]; + for (; src_bucket < stop_bucket; src_bucket++, bucket++) { + memcpy(bucket, src_bucket, sizeof(*src_bucket)); + + if (src_bucket->num_achievements) { + const v1_rc_client_achievement_t** src_achievement = (const v1_rc_client_achievement_t**)src_bucket->achievements; + const v1_rc_client_achievement_t** stop_achievement = src_achievement + src_bucket->num_achievements; + rc_client_achievement_t** achievement = &new_list->achievements_pointers[num_achievements]; + + bucket->achievements = (const rc_client_achievement_t**)achievement; + + for (; src_achievement < stop_achievement; ++src_achievement, ++achievement) { + *achievement = &new_list->achievements[num_achievements++]; + memcpy(*achievement, *src_achievement, sizeof(**src_achievement)); + + (*achievement)->badge_url = rc_client_external_build_avatar_url(badge_url, RC_IMAGE_TYPE_ACHIEVEMENT, (*achievement)->badge_name); + badge_url += RC_CLIENT_IMAGE_URL_BUFFER_SIZE; + (*achievement)->badge_locked_url = rc_client_external_build_avatar_url(badge_url, RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, (*achievement)->badge_name); + badge_url += RC_CLIENT_IMAGE_URL_BUFFER_SIZE; + } + } + } + + new_list->info.public_.num_buckets = v1_achievement_list->num_buckets; + } + + return (rc_client_achievement_list_t*)new_list; +} diff --git a/deps/rcheevos/src/rc_client_external.h b/deps/rcheevos/src/rc_client_external.h index 520a883e13..8efe812a5d 100644 --- a/deps/rcheevos/src/rc_client_external.h +++ b/deps/rcheevos/src/rc_client_external.h @@ -95,12 +95,12 @@ typedef struct rc_client_external_t rc_client_external_begin_identify_and_load_game_func_t begin_identify_and_load_game; rc_client_external_begin_load_game_func_t begin_load_game; rc_client_external_get_game_info_func_t get_game_info; - rc_client_external_begin_load_subset_t begin_load_subset; + rc_client_external_begin_load_subset_t begin_load_subset; /* DEPRECATED */ rc_client_external_get_subset_info_func_t get_subset_info; rc_client_external_action_func_t unload_game; rc_client_external_get_user_game_summary_func_t get_user_game_summary; - rc_client_external_begin_change_media_func_t begin_change_media; - rc_client_external_begin_load_game_func_t begin_change_media_from_hash; + rc_client_external_begin_change_media_func_t begin_identify_and_change_media; + rc_client_external_begin_load_game_func_t begin_change_media; rc_client_external_create_achievement_list_func_t create_achievement_list; rc_client_external_get_int_func_t has_achievements; @@ -129,13 +129,32 @@ typedef struct rc_client_external_t rc_client_external_add_game_hash_func_t add_game_hash; rc_client_external_set_string_func_t load_unknown_game; + /* VERSION 3 */ + rc_client_external_get_user_info_func_t get_user_info_v3; + rc_client_external_get_game_info_func_t get_game_info_v3; + rc_client_external_get_subset_info_func_t get_subset_info_v3; + rc_client_external_get_achievement_info_func_t get_achievement_info_v3; + rc_client_external_create_achievement_list_func_t create_achievement_list_v3; + + /* VERSION 4 */ + rc_client_external_set_int_func_t set_allow_background_memory_reads; + } rc_client_external_t; -#define RC_CLIENT_EXTERNAL_VERSION 2 +#define RC_CLIENT_EXTERNAL_VERSION 4 void rc_client_add_game_hash(rc_client_t* client, const char* hash, uint32_t game_id); void rc_client_load_unknown_game(rc_client_t* client, const char* hash); +/* conversion support */ + +struct rc_client_external_conversions_t; +const rc_client_user_t* rc_client_external_convert_v1_user(const rc_client_t* client, const rc_client_user_t* v1_user); +const rc_client_game_t* rc_client_external_convert_v1_game(const rc_client_t* client, const rc_client_game_t* v1_game); +const rc_client_subset_t* rc_client_external_convert_v1_subset(const rc_client_t* client, const rc_client_subset_t* v1_subset); +const rc_client_achievement_t* rc_client_external_convert_v1_achievement(const rc_client_t* client, const rc_client_achievement_t* v1_achievement); +rc_client_achievement_list_t* rc_client_external_convert_v1_achievement_list(const rc_client_t* client, rc_client_achievement_list_t* v1_achievement_list); + RC_END_C_DECLS #endif /* RC_CLIENT_EXTERNAL_H */ diff --git a/deps/rcheevos/src/rc_client_external_versions.h b/deps/rcheevos/src/rc_client_external_versions.h new file mode 100644 index 0000000000..e8a324dba9 --- /dev/null +++ b/deps/rcheevos/src/rc_client_external_versions.h @@ -0,0 +1,149 @@ +#ifndef RC_CLIENT_EXTERNAL_CONVERSIONS_H +#define RC_CLIENT_EXTERNAL_CONVERSIONS_H + +#include "rc_client_internal.h" + +RC_BEGIN_C_DECLS + +/* user */ + +typedef struct v1_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; +} v1_rc_client_user_t; + +typedef struct v3_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; + const char* avatar_url; +} v3_rc_client_user_t; + +/* game */ + +typedef struct v1_rc_client_game_t { + uint32_t id; + uint32_t console_id; + const char* title; + const char* hash; + const char* badge_name; +} v1_rc_client_game_t; + +typedef struct v3_rc_client_game_t { + uint32_t id; + uint32_t console_id; + const char* title; + const char* hash; + const char* badge_name; + const char* badge_url; +} v3_rc_client_game_t; + +/* subset */ + +typedef struct v1_rc_client_subset_t { + uint32_t id; + const char* title; + char badge_name[16]; + uint32_t num_achievements; + uint32_t num_leaderboards; +} v1_rc_client_subset_t; + +typedef struct v3_rc_client_subset_t { + uint32_t id; + const char* title; + char badge_name[16]; + uint32_t num_achievements; + uint32_t num_leaderboards; + const char* badge_url; +} v3_rc_client_subset_t; + +/* achievement */ + +typedef struct v1_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; + float rarity; + float rarity_hardcore; + uint8_t type; +} v1_rc_client_achievement_t; + +typedef struct v3_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; + float rarity; + float rarity_hardcore; + uint8_t type; + const char* badge_url; + const char* badge_locked_url; +} v3_rc_client_achievement_t; + +/* achievement list */ + +typedef struct v1_rc_client_achievement_bucket_t { + v1_rc_client_achievement_t** achievements; + uint32_t num_achievements; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} v1_rc_client_achievement_bucket_t; + +typedef struct v1_rc_client_achievement_list_t { + v1_rc_client_achievement_bucket_t* buckets; + uint32_t num_buckets; +} v1_rc_client_achievement_list_t; + +typedef struct v1_rc_client_achievement_list_info_t { + v1_rc_client_achievement_list_t public_; + rc_client_destroy_achievement_list_func_t destroy_func; +} v1_rc_client_achievement_list_info_t; + +typedef struct v3_rc_client_achievement_bucket_t { + const v3_rc_client_achievement_t** achievements; + uint32_t num_achievements; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} v3_rc_client_achievement_bucket_t; + +typedef struct v3_rc_client_achievement_list_t { + const v3_rc_client_achievement_bucket_t* buckets; + uint32_t num_buckets; +} v3_rc_client_achievement_list_t; + +typedef struct v3_rc_client_achievement_list_info_t { + v3_rc_client_achievement_list_t public_; + rc_client_destroy_achievement_list_func_t destroy_func; +} v3_rc_client_achievement_list_info_t; + +RC_END_C_DECLS + +#endif /* RC_CLIENT_EXTERNAL_CONVERSIONS_H */ diff --git a/deps/rcheevos/src/rc_client_internal.h b/deps/rcheevos/src/rc_client_internal.h index a27da6588d..156c05c40e 100644 --- a/deps/rcheevos/src/rc_client_internal.h +++ b/deps/rcheevos/src/rc_client_internal.h @@ -9,6 +9,9 @@ #ifdef RC_CLIENT_SUPPORTS_EXTERNAL #include "rc_client_external.h" #endif +#ifdef RC_CLIENT_SUPPORTS_HASH + #include "rhash/rc_hash_internal.h" +#endif #include "rc_compat.h" #include "rc_runtime.h" @@ -20,9 +23,9 @@ RC_BEGIN_C_DECLS | Callbacks | \*****************************************************************************/ -struct rc_api_fetch_game_data_response_t; -typedef void (RC_CCONV *rc_client_post_process_game_data_response_t)(const rc_api_server_response_t* server_response, - struct rc_api_fetch_game_data_response_t* game_data_response, rc_client_t* client, void* userdata); +struct rc_api_fetch_game_sets_response_t; +typedef void (RC_CCONV *rc_client_post_process_game_sets_response_t)(const rc_api_server_response_t* server_response, + struct rc_api_fetch_game_sets_response_t* game_sets_response, rc_client_t* client, void* userdata); typedef int (RC_CCONV *rc_client_can_submit_achievement_unlock_t)(uint32_t achievement_id, rc_client_t* client); typedef int (RC_CCONV *rc_client_can_submit_leaderboard_entry_t)(uint32_t leaderboard_id, rc_client_t* client); typedef int (RC_CCONV *rc_client_rich_presence_override_t)(rc_client_t* client, char buffer[], size_t buffersize); @@ -36,11 +39,15 @@ typedef struct rc_client_callbacks_t { rc_client_message_callback_t log_call; rc_get_time_millisecs_func_t get_time_millisecs; rc_client_identify_hash_func_t identify_unknown_hash; - rc_client_post_process_game_data_response_t post_process_game_data_response; + rc_client_post_process_game_sets_response_t post_process_game_sets_response; rc_client_can_submit_achievement_unlock_t can_submit_achievement_unlock; rc_client_can_submit_leaderboard_entry_t can_submit_leaderboard_entry; rc_client_rich_presence_override_t rich_presence_override; +#ifdef RC_CLIENT_SUPPORTS_HASH + rc_hash_callbacks_t hash; +#endif + void* client_data; } rc_client_callbacks_t; @@ -215,14 +222,13 @@ typedef struct rc_client_subset_info_t { uint8_t pending_events; } rc_client_subset_info_t; -rc_client_async_handle_t* rc_client_begin_load_subset(rc_client_t* client, uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata); - /*****************************************************************************\ | Game | \*****************************************************************************/ typedef struct rc_client_game_hash_t { char hash[33]; + uint8_t is_unknown; uint32_t game_id; struct rc_client_game_hash_t* next; } rc_client_game_hash_t; @@ -300,9 +306,11 @@ typedef struct rc_client_state_t { rc_buffer_t buffer; rc_client_scheduled_callback_data_t* scheduled_callbacks; + rc_api_host_t host; #ifdef RC_CLIENT_SUPPORTS_EXTERNAL rc_client_external_t* external_client; + struct rc_client_external_conversions_t* external_client_conversions; #endif #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION rc_client_raintegration_t* raintegration; @@ -319,6 +327,7 @@ typedef struct rc_client_state_t { uint8_t user; uint8_t disconnect; uint8_t allow_leaderboards_in_softcore; + uint8_t allow_background_memory_reads; struct rc_client_load_state_t* load; struct rc_client_async_handle_t* async_handles[4]; diff --git a/deps/rcheevos/src/rc_client_raintegration.c b/deps/rcheevos/src/rc_client_raintegration.c index a686f7fd3c..8134bff2fd 100644 --- a/deps/rcheevos/src/rc_client_raintegration.c +++ b/deps/rcheevos/src/rc_client_raintegration.c @@ -6,6 +6,24 @@ #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION +/* ===== natvis extensions ===== */ + +typedef struct __rc_client_raintegration_event_enum_t { uint8_t value; } __rc_client_raintegration_event_enum_t; +static void rc_client_raintegration_natvis_helper(void) +{ + struct natvis_extensions { + __rc_client_raintegration_event_enum_t raintegration_event_type; + } natvis; + + natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_TYPE_NONE; + natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED; + natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED; + natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_PAUSE; + natvis.raintegration_event_type.value = RC_CLIENT_RAINTEGRATION_EVENT_MENU_CHANGED; +} + +/* ============================= */ + static void rc_client_raintegration_load_dll(rc_client_t* client, const wchar_t* search_directory, rc_client_callback_t callback, void* callback_userdata) { @@ -89,6 +107,9 @@ static void rc_client_raintegration_load_dll(rc_client_t* client, FreeLibrary(hDLL); callback(RC_ABORTED, "One or more required exports was not found in RA_Integration.dll", client, callback_userdata); + + /* dummy reference to natvis helper to ensure extensions get compiled in. */ + raintegration->shutdown = rc_client_raintegration_natvis_helper; } else { rc_mutex_lock(&client->state.mutex); @@ -151,7 +172,7 @@ static void rc_client_init_raintegration(rc_client_t* client, const char* host_url = client->state.raintegration->get_host_url(); if (host_url) { if (strcmp(host_url, "OFFLINE") != 0) { - if (strcmp(host_url, "https://retroachievements.org") != 0) + if (strcmp(host_url, rc_api_default_host()) != 0) rc_client_set_host(client, host_url); } else if (client->state.raintegration->init_client_offline) { @@ -323,7 +344,7 @@ rc_client_async_handle_t* rc_client_begin_load_raintegration(rc_client_t* client if (client->state.raintegration->get_host_url) { const char* host_url = client->state.raintegration->get_host_url(); - if (host_url && strcmp(host_url, "https://retroachievements.org") != 0 && + if (host_url && strcmp(host_url, rc_api_default_host()) != 0 && strcmp(host_url, "OFFLINE") != 0) { /* if the DLL specifies a custom host, use it */ rc_client_set_host(client, host_url); @@ -331,7 +352,7 @@ rc_client_async_handle_t* rc_client_begin_load_raintegration(rc_client_t* client } memset(&request, 0, sizeof(request)); - rc_api_url_build_dorequest_url(&request); + rc_api_url_build_dorequest_url(&request, &client->state.host); rc_url_builder_init(&builder, &request.buffer, 48); rc_url_builder_append_str_param(&builder, "r", "latestintegration"); request.post_data = rc_url_builder_finalize(&builder); diff --git a/deps/rcheevos/src/rc_compat.c b/deps/rcheevos/src/rc_compat.c index 80044d37de..0ab9e4dda5 100644 --- a/deps/rcheevos/src/rc_compat.c +++ b/deps/rcheevos/src/rc_compat.c @@ -1,5 +1,15 @@ +#if !defined(RC_NO_THREADS) && !defined(_WIN32) && !defined(GEKKO) && !defined(_3DS) && (!defined(_XOPEN_SOURCE) || (_XOPEN_SOURCE - 0) < 500) +/* We'll want to use pthread_mutexattr_settype/PTHREAD_MUTEX_RECURSIVE, but glibc only conditionally exposes pthread_mutexattr_settype and PTHREAD_MUTEX_RECURSIVE depending on feature flags + * Defining _XOPEN_SOURCE must be done at the top of the source file, before including any headers + * pthread_mutexattr_settype/PTHREAD_MUTEX_RECURSIVE are specified the Single UNIX Specification (Version 2, 1997), along with POSIX later on (IEEE Standard 1003.1-2008), so should cover practically any pthread implementation + */ +#undef _XOPEN_SOURCE +#define _XOPEN_SOURCE 500 +#endif + #include "rc_compat.h" +#include #include #include @@ -58,7 +68,7 @@ int rc_snprintf(char* buffer, size_t size, const char* format, ...) va_start(args, format); -#ifdef __STDC_WANT_SECURE_LIB__ +#ifdef __STDC_SECURE_LIB__ result = vsprintf_s(buffer, size, format, args); #else /* assume buffer is large enough and ignore size */ @@ -73,7 +83,7 @@ int rc_snprintf(char* buffer, size_t size, const char* format, ...) #endif -#ifndef __STDC_WANT_SECURE_LIB__ +#ifndef __STDC_SECURE_LIB__ struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer) { @@ -88,32 +98,79 @@ struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer) #if defined(_WIN32) -/* https://gist.github.com/roxlu/1c1af99f92bafff9d8d9 */ +/* https://learn.microsoft.com/en-us/archive/msdn-magazine/2012/november/windows-with-c-the-evolution-of-synchronization-in-windows-and-c */ +/* implementation largely taken from https://github.com/libsdl-org/SDL/blob/0fc3574/src/thread/windows/SDL_sysmutex.c */ + +#if defined(WINVER) && WINVER >= 0x0600 -#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); + InitializeSRWLock(&mutex->srw_lock); + /* https://learn.microsoft.com/en-us/windows/win32/procthread/thread-handles-and-identifiers */ + /* thread ids are never 0 */ + mutex->owner = 0; + mutex->count = 0; } void rc_mutex_destroy(rc_mutex_t* mutex) { - CloseHandle(mutex->handle); + /* Nothing to do here */ + (void)mutex; } void rc_mutex_lock(rc_mutex_t* mutex) { - WaitForSingleObject(mutex->handle, 0xFFFFFFFF); + DWORD current_thread = GetCurrentThreadId(); + if (mutex->owner == current_thread) { + ++mutex->count; + assert(mutex->count > 0); + } + else { + AcquireSRWLockExclusive(&mutex->srw_lock); + assert(mutex->owner == 0 && mutex->count == 0); + mutex->owner = current_thread; + mutex->count = 1; + } } void rc_mutex_unlock(rc_mutex_t* mutex) { - ReleaseMutex(mutex->handle); + if (mutex->owner == GetCurrentThreadId()) { + assert(mutex->count > 0); + if (--mutex->count == 0) { + mutex->owner = 0; + ReleaseSRWLockExclusive(&mutex->srw_lock); + } + } + else { + assert(!"Tried to unlock unowned mutex"); + } } +#else + +void rc_mutex_init(rc_mutex_t* mutex) +{ + InitializeCriticalSection(&mutex->critical_section); +} + +void rc_mutex_destroy(rc_mutex_t* mutex) +{ + DeleteCriticalSection(&mutex->critical_section); +} + +void rc_mutex_lock(rc_mutex_t* mutex) +{ + EnterCriticalSection(&mutex->critical_section); +} + +void rc_mutex_unlock(rc_mutex_t* mutex) +{ + LeaveCriticalSection(&mutex->critical_section); +} + +#endif + #elif defined(GEKKO) /* https://github.com/libretro/RetroArch/pull/16116 */ @@ -167,7 +224,12 @@ void rc_mutex_unlock(rc_mutex_t* mutex) void rc_mutex_init(rc_mutex_t* mutex) { - pthread_mutex_init(mutex, NULL); + /* Define the mutex as recursive, for consistent semantics against other rc_mutex_t implementations */ + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(mutex, &attr); + pthread_mutexattr_destroy(&attr); } void rc_mutex_destroy(rc_mutex_t* mutex) diff --git a/deps/rcheevos/src/rc_compat.h b/deps/rcheevos/src/rc_compat.h index 614d1bc273..3f24df2efd 100644 --- a/deps/rcheevos/src/rc_compat.h +++ b/deps/rcheevos/src/rc_compat.h @@ -1,6 +1,13 @@ #ifndef RC_COMPAT_H #define RC_COMPAT_H +#ifdef _WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #include +#endif + #include "rc_export.h" #include @@ -58,7 +65,7 @@ RC_BEGIN_C_DECLS #endif /* __STDC_VERSION__ < 199901L */ -#ifndef __STDC_WANT_SECURE_LIB__ +#ifndef __STDC_SECURE_LIB__ /* _CRT_SECURE_NO_WARNINGS redefinitions */ #define strcpy_s(dest, sz, src) strcpy(dest, src) #define sscanf_s sscanf @@ -77,10 +84,19 @@ RC_BEGIN_C_DECLS #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; + #if defined(_WIN32) + typedef struct rc_mutex_t { + #if defined(WINVER) && WINVER >= 0x0600 + /* Windows Vista and later can use a slim reader/writer (SRW) lock */ + SRWLOCK srw_lock; + /* Current thread owner needs to be tracked (for recursive mutex usage) */ + DWORD owner; + DWORD count; + #else + /* Pre-Vista must use a critical section */ + CRITICAL_SECTION critical_section; + #endif + } rc_mutex_t; #elif defined(GEKKO) #include typedef struct rc_mutex_t { diff --git a/deps/rcheevos/src/rc_libretro.c b/deps/rcheevos/src/rc_libretro.c index d343ce78ce..66d134366a 100644 --- a/deps/rcheevos/src/rc_libretro.c +++ b/deps/rcheevos/src/rc_libretro.c @@ -10,20 +10,11 @@ #include "rc_consoles.h" #include "rc_compat.h" +#include "rhash/rc_hash_internal.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. @@ -87,6 +78,7 @@ static const rc_disallowed_setting_t _rc_disallowed_fbneo_settings[] = { }; static const rc_disallowed_setting_t _rc_disallowed_fceumm_settings[] = { + { "fceumm_game_genie", "!disabled" }, { "fceumm_region", ",PAL,Dendy" }, { NULL, NULL } }; @@ -124,7 +116,7 @@ static const rc_disallowed_setting_t _rc_disallowed_neocd_settings[] = { }; static const rc_disallowed_setting_t _rc_disallowed_pcsx_rearmed_settings[] = { - { "pcsx_rearmed_psxclock", "<55" }, + { "pcsx_rearmed_psxclock", ",!auto,<55" }, { "pcsx_rearmed_region", "pal" }, { NULL, NULL } }; @@ -202,14 +194,14 @@ static const rc_disallowed_core_settings_t rc_disallowed_core_settings[] = { { NULL, NULL } }; -static int rc_libretro_string_equal_nocase_wildcard(const char* test, const char* value) { +static int rc_libretro_string_equal_nocase_wildcard(const char* test, const char* match) { char c1, c2; while ((c1 = *test++)) { - if (tolower(c1) != tolower(c2 = *value++) && c2 != '?') + if (tolower(c1) != tolower(c2 = *match++) && c2 != '?') return (c2 == '*'); } - return (*value == '\0'); + return (*match == '\0'); } static int rc_libretro_numeric_less_than(const char* test, const char* value) { @@ -218,7 +210,50 @@ static int rc_libretro_numeric_less_than(const char* test, const char* value) { return (test_num < value_num); } +static int rc_libretro_match_token(const char* val, const char* token, size_t size, int* result) { + if (*token == '!') { + /* !X => if X is a match, it's explicitly allowed. match with result = false */ + if (rc_libretro_match_token(val, token + 1, size - 1, result)) { + *result = 0; + return 1; + } + } + + if (*token == '<') { + /* if val < token, match with result = true */ + char buffer[128]; + memcpy(buffer, token + 1, size - 1); + buffer[size - 1] = '\0'; + if (rc_libretro_numeric_less_than(val, buffer)) { + *result = 1; + return 1; + } + } + + if (memcmp(token, val, size) == 0 && val[size] == 0) { + /* exact match, match with result = true */ + *result = 1; + return 1; + } + else { + /* check for case insensitive match */ + char buffer[128]; + memcpy(buffer, token, size); + buffer[size] = '\0'; + if (rc_libretro_string_equal_nocase_wildcard(val, buffer)) { + /* case insensitive match, match with result = true */ + *result = 1; + return 1; + } + } + + /* no match */ + return 0; +} + static int rc_libretro_match_value(const char* val, const char* match) { + int result = 0; + /* if value starts with a comma, it's a CSV list of potential matches */ if (*match == ',') { do { @@ -229,33 +264,23 @@ static int rc_libretro_match_value(const char* val, const char* 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 == ','); + if (rc_libretro_match_token(val, ptr, size, &result)) + return result; - return 0; + } while (*match == ','); + } + else { + /* 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 */ + if (rc_libretro_match_token(val, match, strlen(match), &result)) + return result; } - /* a leading exclamation point means the provided value(s) are not forbidden (are allowed) */ - if (*match == '!') - return !rc_libretro_match_value(val, &match[1]); - - /* a leading less tahn means the provided value is the minimum allowed */ - if (*match == '<') - return rc_libretro_numeric_less_than(val, &match[1]); - - /* just a single value, attempt to match it */ - return rc_libretro_string_equal_nocase_wildcard(val, match); + /* value did not match filters, assume it's allowed */ + return 0; } int rc_libretro_is_setting_allowed(const rc_disallowed_setting_t* disallowed_settings, const char* setting, const char* value) { @@ -711,7 +736,8 @@ void rc_libretro_memory_destroy(rc_libretro_memory_regions_t* 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) { + const char* m3u_path, rc_libretro_get_image_path_func get_image_path, + const rc_hash_filereader_t* file_reader) { char image_path[1024]; char* m3u_contents; char* ptr; @@ -724,23 +750,24 @@ void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, if (!rc_path_compare_extension(m3u_path, "m3u")) return; - file_handle = rc_file_open(m3u_path); + file_handle = file_reader->open(m3u_path); if (!file_handle) { - rc_hash_error("Could not open playlist"); + rc_hash_iterator_t iterator; + memset(&iterator, 0, sizeof(iterator)); + memcpy(&iterator.callbacks, &hash_set->callbacks, sizeof(hash_set->callbacks)); + rc_hash_iterator_error(&iterator, "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); + file_reader->seek(file_handle, 0, SEEK_END); + file_len = file_reader->tell(file_handle); + file_reader->seek(file_handle, 0, SEEK_SET); m3u_contents = (char*)malloc((size_t)file_len + 1); if (m3u_contents) { - rc_file_read(file_handle, m3u_contents, (int)file_len); + file_reader->read(file_handle, m3u_contents, (int)file_len); m3u_contents[file_len] = '\0'; - rc_file_close(file_handle); - ptr = m3u_contents; do { @@ -774,6 +801,9 @@ void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set, free(m3u_contents); } + if (file_reader->close) + file_reader->close(file_handle); + 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 */ diff --git a/deps/rcheevos/src/rc_libretro.h b/deps/rcheevos/src/rc_libretro.h index 66ab9a3528..8821303218 100644 --- a/deps/rcheevos/src/rc_libretro.h +++ b/deps/rcheevos/src/rc_libretro.h @@ -3,6 +3,8 @@ #include "rc_export.h" +#include "rc_hash.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 @@ -75,12 +77,15 @@ typedef struct rc_libretro_hash_set_t struct rc_libretro_hash_entry_t* entries; uint16_t entries_count; uint16_t entries_size; + + rc_hash_callbacks_t callbacks; } rc_libretro_hash_set_t; typedef int (RC_CCONV *rc_libretro_get_image_path_func)(uint32_t index, char* buffer, size_t buffer_size); RC_EXPORT void RC_CCONV 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); + const char* m3u_path, rc_libretro_get_image_path_func get_image_path, + const rc_hash_filereader_t* file_reader); RC_EXPORT void RC_CCONV rc_libretro_hash_set_destroy(struct rc_libretro_hash_set_t* hash_set); RC_EXPORT void RC_CCONV rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set, diff --git a/deps/rcheevos/src/rc_util.c b/deps/rcheevos/src/rc_util.c index b6aa5bf6d6..cd750d2c3c 100644 --- a/deps/rcheevos/src/rc_util.c +++ b/deps/rcheevos/src/rc_util.c @@ -38,6 +38,9 @@ void rc_buffer_destroy(rc_buffer_t* buffer) total += (int)(chunk->end - chunk->start); wasted += (int)(chunk->end - chunk->write); ++count; +#endif +#ifdef DEBUG_BUFFERS + printf("< free %p.%p\n", (void*)buffer, (void*)chunk); #endif free(chunk); chunk = next; @@ -70,6 +73,10 @@ uint8_t* rc_buffer_reserve(rc_buffer_t* buffer, size_t amount) if (!chunk->next) break; +#ifdef DEBUG_BUFFERS + printf("> alloc %p.%p\n", (void*)buffer, (void*)chunk->next); +#endif + chunk->next->start = (uint8_t*)chunk->next + chunk_header_size; chunk->next->write = chunk->next->start; chunk->next->end = (uint8_t*)chunk->next + alloc_size; @@ -148,7 +155,7 @@ const char* rc_error_str(int ret) { switch (ret) { case RC_OK: return "OK"; - case RC_INVALID_LUA_OPERAND: return "Invalid Lua operand"; + case RC_INVALID_FUNC_OPERAND: return "Invalid function operand"; case RC_INVALID_MEMORY_OPERAND: return "Invalid memory operand"; case RC_INVALID_CONST_OPERAND: return "Invalid constant operand"; case RC_INVALID_FP_OPERAND: return "Invalid floating-point operand"; @@ -186,6 +193,7 @@ const char* rc_error_str(int ret) case RC_INSUFFICIENT_BUFFER: return "Buffer not large enough"; case RC_INVALID_VARIABLE_NAME: return "Invalid variable name"; case RC_UNKNOWN_VARIABLE_NAME: return "Unknown variable name"; + case RC_NOT_FOUND: return "Not found"; default: return "Unknown error"; } } diff --git a/deps/rcheevos/src/rc_version.h b/deps/rcheevos/src/rc_version.h index 4f8c25e364..bc8add3017 100644 --- a/deps/rcheevos/src/rc_version.h +++ b/deps/rcheevos/src/rc_version.h @@ -7,8 +7,8 @@ RC_BEGIN_C_DECLS -#define RCHEEVOS_VERSION_MAJOR 11 -#define RCHEEVOS_VERSION_MINOR 6 +#define RCHEEVOS_VERSION_MAJOR 12 +#define RCHEEVOS_VERSION_MINOR 0 #define RCHEEVOS_VERSION_PATCH 0 #define RCHEEVOS_MAKE_VERSION(major, minor, patch) (major * 1000000 + minor * 1000 + patch) diff --git a/deps/rcheevos/src/rcheevos/alloc.c b/deps/rcheevos/src/rcheevos/alloc.c index 0aa4e5cb5f..5dbf20cb0f 100644 --- a/deps/rcheevos/src/rcheevos/alloc.c +++ b/deps/rcheevos/src/rcheevos/alloc.c @@ -35,11 +35,11 @@ void* rc_alloc(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment if (pointer != 0) { /* valid buffer, grab the next chunk */ - ptr = (void*)((char*)pointer + *offset); + ptr = (void*)((uint8_t*)pointer + *offset); } else if (scratch != 0 && scratch_object_pointer_offset < sizeof(scratch->objs)) { /* only allocate one instance of each object type (indentified by scratch_object_pointer_offset) */ - void** scratch_object_pointer = (void**)((char*)&scratch->objs + scratch_object_pointer_offset); + void** scratch_object_pointer = (void**)((uint8_t*)&scratch->objs + scratch_object_pointer_offset); ptr = *scratch_object_pointer; if (!ptr) { int32_t used; @@ -94,22 +94,216 @@ char* rc_alloc_str(rc_parse_state_t* parse, const char* text, size_t length) { return ptr; } -void rc_init_parse_state(rc_parse_state_t* parse, void* buffer, lua_State* L, int funcs_ndx) +void rc_init_preparse_state(rc_preparse_state_t* preparse) +{ + rc_init_parse_state(&preparse->parse, NULL); + rc_init_parse_state_memrefs(&preparse->parse, &preparse->memrefs); +} + +void rc_destroy_preparse_state(rc_preparse_state_t* preparse) +{ + rc_destroy_parse_state(&preparse->parse); +} + +void rc_preparse_alloc_memrefs(rc_memrefs_t* memrefs, rc_preparse_state_t* preparse) +{ + const uint32_t num_memrefs = rc_memrefs_count_memrefs(&preparse->memrefs); + const uint32_t num_modified_memrefs = rc_memrefs_count_modified_memrefs(&preparse->memrefs); + + if (preparse->parse.offset < 0) + return; + + if (memrefs) { + memset(memrefs, 0, sizeof(*memrefs)); + preparse->parse.memrefs = memrefs; + } + + if (num_memrefs) { + rc_memref_t* memref_items = RC_ALLOC_ARRAY(rc_memref_t, num_memrefs, &preparse->parse); + + if (memrefs) { + memrefs->memrefs.capacity = num_memrefs; + memrefs->memrefs.items = memref_items; + } + } + + if (num_modified_memrefs) { + rc_modified_memref_t* modified_memref_items = + RC_ALLOC_ARRAY(rc_modified_memref_t, num_modified_memrefs, &preparse->parse); + + if (memrefs) { + memrefs->modified_memrefs.capacity = num_modified_memrefs; + memrefs->modified_memrefs.items = modified_memref_items; + } + } + + /* when preparsing, this structure will be allocated at the end. when it's allocated earlier + * in the buffer, it could be followed by something aligned at 8 bytes. force the offset to + * an 8-byte boundary */ + if (!memrefs) { + rc_alloc(preparse->parse.buffer, &preparse->parse.offset, 0, 8, &preparse->parse.scratch, 0); + } +} + +static uint32_t rc_preparse_array_size(uint32_t needed, uint32_t minimum) +{ + while (minimum < needed) + minimum <<= 1; + + return minimum; +} + +void rc_preparse_reserve_memrefs(rc_preparse_state_t* preparse, rc_memrefs_t* memrefs) +{ + uint32_t num_memrefs = rc_memrefs_count_memrefs(&preparse->memrefs); + uint32_t num_modified_memrefs = rc_memrefs_count_modified_memrefs(&preparse->memrefs); + uint32_t available; + + if (preparse->parse.offset < 0) + return; + + if (num_memrefs) { + rc_memref_list_t* memref_list = &memrefs->memrefs; + while (memref_list->count == memref_list->capacity) { + if (!memref_list->next) + break; + + memref_list = memref_list->next; + } + + available = memref_list->capacity - memref_list->count; + if (available < num_memrefs) { + rc_memref_list_t* new_memref_list = (rc_memref_list_t*)calloc(1, sizeof(rc_memref_list_t)); + if (!new_memref_list) + return; + + new_memref_list->capacity = rc_preparse_array_size(num_memrefs - available, 16); + new_memref_list->items = (rc_memref_t*)malloc(new_memref_list->capacity * sizeof(rc_memref_t)); + new_memref_list->allocated = 1; + memref_list->next = new_memref_list; + } + } + + if (num_modified_memrefs) { + rc_modified_memref_list_t* modified_memref_list = &memrefs->modified_memrefs; + while (modified_memref_list->count == modified_memref_list->capacity) { + if (!modified_memref_list->next) + break; + + modified_memref_list = modified_memref_list->next; + } + + available = modified_memref_list->capacity - modified_memref_list->count; + if (available < num_modified_memrefs) { + rc_modified_memref_list_t* new_modified_memref_list = (rc_modified_memref_list_t*)calloc(1, sizeof(rc_modified_memref_list_t)); + if (!new_modified_memref_list) + return; + + new_modified_memref_list->capacity = rc_preparse_array_size(num_modified_memrefs - available, 8); + new_modified_memref_list->items = (rc_modified_memref_t*)malloc(new_modified_memref_list->capacity * sizeof(rc_modified_memref_t)); + new_modified_memref_list->allocated = 1; + modified_memref_list->next = new_modified_memref_list; + } + } + + preparse->parse.memrefs = memrefs; +} + +static void rc_preparse_sync_operand(rc_operand_t* operand, rc_parse_state_t* parse, const rc_memrefs_t* memrefs) +{ + if (rc_operand_is_memref(operand) || rc_operand_is_recall(operand)) { + const rc_memref_t* src_memref = operand->value.memref; + + if (src_memref->value.memref_type == RC_MEMREF_TYPE_MODIFIED_MEMREF) { + const rc_modified_memref_list_t* modified_memref_list = &memrefs->modified_memrefs; + for (; modified_memref_list; modified_memref_list = modified_memref_list->next) { + const rc_modified_memref_t* modified_memref = modified_memref_list->items; + const rc_modified_memref_t* modified_memref_end = modified_memref + modified_memref_list->count; + + for (; modified_memref < modified_memref_end; ++modified_memref) { + if ((const rc_modified_memref_t*)src_memref == modified_memref) { + rc_modified_memref_t* dst_modified_memref = rc_alloc_modified_memref(parse, modified_memref->memref.value.size, + &modified_memref->parent, modified_memref->modifier_type, &modified_memref->modifier); + + operand->value.memref = &dst_modified_memref->memref; + return; + } + } + } + } + else { + const rc_memref_list_t* memref_list = &memrefs->memrefs; + for (; memref_list; memref_list = memref_list->next) { + const rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_end = memref + memref_list->count; + + for (; memref < memref_end; ++memref) { + if (src_memref == memref) { + operand->value.memref = rc_alloc_memref(parse, memref->address, memref->value.size); + return; + } + } + } + } + } +} + +void rc_preparse_copy_memrefs(rc_parse_state_t* parse, rc_memrefs_t* memrefs) +{ + const rc_memref_list_t* memref_list = &memrefs->memrefs; + const rc_modified_memref_list_t* modified_memref_list = &memrefs->modified_memrefs; + + for (; memref_list; memref_list = memref_list->next) { + const rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_end = memref + memref_list->count; + + for (; memref < memref_end; ++memref) + rc_alloc_memref(parse, memref->address, memref->value.size); + } + + for (; modified_memref_list; modified_memref_list = modified_memref_list->next) { + rc_modified_memref_t* modified_memref = modified_memref_list->items; + const rc_modified_memref_t* modified_memref_end = modified_memref + modified_memref_list->count; + + for (; modified_memref < modified_memref_end; ++modified_memref) { + rc_preparse_sync_operand(&modified_memref->parent, parse, memrefs); + rc_preparse_sync_operand(&modified_memref->modifier, parse, memrefs); + + rc_alloc_modified_memref(parse, modified_memref->memref.value.size, + &modified_memref->parent, modified_memref->modifier_type, &modified_memref->modifier); + } + } +} + +void rc_reset_parse_state(rc_parse_state_t* parse, void* buffer) { - /* could use memset here, but rc_parse_state_t contains a 512 byte buffer that doesn't need to be initialized */ - parse->offset = 0; - parse->L = L; - parse->funcs_ndx = funcs_ndx; parse->buffer = buffer; - parse->scratch.strings = NULL; - rc_buffer_init(&parse->scratch.buffer); - memset(&parse->scratch.objs, 0, sizeof(parse->scratch.objs)); - parse->first_memref = 0; - parse->variables = 0; + + parse->offset = 0; + parse->memrefs = NULL; + parse->existing_memrefs = NULL; + parse->variables = NULL; parse->measured_target = 0; parse->lines_read = 0; + parse->addsource_oper = RC_OPERATOR_NONE; + parse->addsource_parent.type = RC_OPERAND_NONE; + parse->indirect_parent.type = RC_OPERAND_NONE; + parse->remember.type = RC_OPERAND_NONE; + parse->is_value = 0; parse->has_required_hits = 0; parse->measured_as_percent = 0; + parse->ignore_non_parse_errors = 0; + + parse->scratch.strings = NULL; +} + +void rc_init_parse_state(rc_parse_state_t* parse, void* buffer) +{ + /* could use memset here, but rc_parse_state_t contains a 512 byte buffer that doesn't need to be initialized */ + rc_buffer_init(&parse->scratch.buffer); + memset(&parse->scratch.objs, 0, sizeof(parse->scratch.objs)); + + rc_reset_parse_state(parse, buffer); } void rc_destroy_parse_state(rc_parse_state_t* parse) diff --git a/deps/rcheevos/src/rcheevos/condition.c b/deps/rcheevos/src/rcheevos/condition.c index de8efd924f..320ca0a62f 100644 --- a/deps/rcheevos/src/rcheevos/condition.c +++ b/deps/rcheevos/src/rcheevos/condition.c @@ -1,6 +1,7 @@ #include "rc_internal.h" #include +#include #include static int rc_test_condition_compare(uint32_t value1, uint32_t value2, uint8_t oper) { @@ -15,7 +16,7 @@ static int rc_test_condition_compare(uint32_t value1, uint32_t value2, uint8_t o } } -static char rc_condition_determine_comparator(const rc_condition_t* self) { +static uint8_t rc_condition_determine_comparator(const rc_condition_t* self) { switch (self->oper) { case RC_OPERATOR_EQ: case RC_OPERATOR_NE: @@ -31,7 +32,8 @@ static char rc_condition_determine_comparator(const rc_condition_t* self) { } 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)) { + /* TODO: allow modified memref comparisons */ + self->operand1.value.memref->value.memref_type == RC_MEMREF_TYPE_MEMREF && !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); @@ -43,7 +45,7 @@ static char rc_condition_determine_comparator(const rc_condition_t* self) { 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)) { + self->operand2.value.memref->value.memref_type == RC_MEMREF_TYPE_MEMREF && !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); @@ -161,17 +163,45 @@ static int rc_parse_operator(const char** memaddr) { } } -rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, uint8_t is_indirect) { - rc_condition_t* self; +void rc_condition_convert_to_operand(const rc_condition_t* condition, rc_operand_t* operand, rc_parse_state_t* parse) { + if (condition->oper == RC_OPERATOR_NONE) { + if (operand != &condition->operand1) + memcpy(operand, &condition->operand1, sizeof(*operand)); + } + else { + uint8_t new_size = RC_MEMSIZE_32_BITS; + if (rc_operand_is_float(&condition->operand1) || rc_operand_is_float(&condition->operand2)) + new_size = RC_MEMSIZE_FLOAT; + + /* NOTE: this makes the operand include the modification, but we have to also + * leave the modification in the condition so the condition reflects the actual + * definition. This doesn't affect the evaluation logic since this method is only + * called for combining conditions and Measured, and the Measured handling function + * ignores the operator assuming it's been handled by a modified memref chain */ + operand->value.memref = (rc_memref_t*)rc_alloc_modified_memref(parse, + new_size, &condition->operand1, condition->oper, &condition->operand2); + + /* not actually an address, just a non-delta memref read */ + operand->type = operand->memref_access_type = RC_OPERAND_ADDRESS; + + operand->size = new_size; + } +} + +rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse) { + rc_condition_t * self = RC_ALLOC(rc_condition_t, parse); + rc_parse_condition_internal(self, memaddr, parse); + return (parse->offset < 0) ? NULL : self; +} + +void rc_parse_condition_internal(rc_condition_t* self, const char** memaddr, rc_parse_state_t* parse) { const char* aux; int result; int can_modify = 0; aux = *memaddr; - self = RC_ALLOC(rc_condition_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] == ':') { @@ -194,8 +224,11 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse parse->measured_as_percent = 1; self->type = RC_CONDITION_MEASURED; break; + /* e f h j l s u v w x y */ - default: parse->offset = RC_INVALID_CONDITION_TYPE; return 0; + default: + parse->offset = RC_INVALID_CONDITION_TYPE; + return; } aux += 2; @@ -204,79 +237,65 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse self->type = RC_CONDITION_STANDARD; } - result = rc_parse_operand(&self->operand1, &aux, is_indirect, parse); + result = rc_parse_operand(&self->operand1, &aux, parse); if (result < 0) { parse->offset = result; - return 0; + return; } result = rc_parse_operator(&aux); if (result < 0) { parse->offset = result; - return 0; + return; } - self->oper = (char)result; - switch (self->oper) { - case RC_OPERATOR_NONE: - /* non-modifying statements must have a second operand */ - if (!can_modify) { - /* measured does not require a second operand when used in a value */ - if (self->type != RC_CONDITION_MEASURED) { - parse->offset = RC_INVALID_OPERATOR; - return 0; - } + self->oper = (uint8_t)result; + + if (self->oper == RC_OPERATOR_NONE) { + /* non-modifying statements must have a second operand */ + if (!can_modify) { + /* measured does not require a second operand when used in a value */ + if (self->type != RC_CONDITION_MEASURED && !parse->ignore_non_parse_errors) { + parse->offset = RC_INVALID_OPERATOR; + return; } + } - /* provide dummy operand of '1' and no required hits */ - self->operand2.type = RC_OPERAND_CONST; - self->operand2.value.num = 1; - self->required_hits = 0; - *memaddr = aux; - return self; + /* provide dummy operand of '1' and no required hits */ + rc_operand_set_const(&self->operand2, 1); + self->required_hits = 0; + *memaddr = aux; + return; + } - case RC_OPERATOR_MULT: - case RC_OPERATOR_DIV: - case RC_OPERATOR_AND: - case RC_OPERATOR_XOR: - case RC_OPERATOR_MOD: - case RC_OPERATOR_ADD: - case RC_OPERATOR_SUB: - /* modifying operators are only valid on modifying statements */ - if (can_modify) + if (can_modify && !rc_operator_is_modifying(self->oper)) { + /* comparison operators are not valid on modifying statements */ + switch (self->type) { + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + case RC_CONDITION_ADD_ADDRESS: + /* prevent parse errors on legacy achievements where a condition was present before changing the type */ + self->oper = RC_OPERATOR_NONE; break; - /* fallthrough */ - default: - /* comparison operators are not valid on modifying statements */ - if (can_modify) { - switch (self->type) { - case RC_CONDITION_ADD_SOURCE: - case RC_CONDITION_SUB_SOURCE: - case RC_CONDITION_ADD_ADDRESS: - case RC_CONDITION_REMEMBER: - /* prevent parse errors on legacy achievements where a condition was present before changing the type */ - self->oper = RC_OPERATOR_NONE; - break; - - default: - parse->offset = RC_INVALID_OPERATOR; - return 0; + default: + if (!parse->ignore_non_parse_errors) { + parse->offset = RC_INVALID_OPERATOR; + return; } - } - break; + break; + } } - result = rc_parse_operand(&self->operand2, &aux, is_indirect, parse); + result = rc_parse_operand(&self->operand2, &aux, parse); if (result < 0) { parse->offset = result; - return 0; + return; } if (self->oper == RC_OPERATOR_NONE) { /* if operator is none, explicitly clear out the right side */ - self->operand2.type = RC_OPERAND_CONST; - self->operand2.value.num = 0; + rc_operand_set_const(&self->operand2, 0); } if (*aux == '(') { @@ -285,7 +304,7 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse if (end == aux || *end != ')') { parse->offset = RC_INVALID_REQUIRED_HITS; - return 0; + return; } /* if operator is none, explicitly clear out the required hits */ @@ -302,7 +321,7 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse if (end == aux || *end != '.') { parse->offset = RC_INVALID_REQUIRED_HITS; - return 0; + return; } /* if operator is none, explicitly clear out the required hits */ @@ -321,7 +340,185 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse self->optimized_comparator = rc_condition_determine_comparator(self); *memaddr = aux; - return self; +} + +void rc_condition_update_parse_state(rc_condition_t* condition, rc_parse_state_t* parse) { + /* type of values in the chain are determined by the parent. + * the last element of a chain is determined by the operand + * + * 1 + 1.5 + 1.75 + 1.0 => (int)1 + (int)1 + (int)1 + (float)1 = (float)4.0 + * 1.0 + 1.5 + 1.75 + 1.0 => (float)1.0 + (float)1.5 + (float)1.75 + (float)1.0 = (float)5.25 + * 1.0 + 1.5 + 1.75 + 1 => (float)1.0 + (float)1.5 + (float)1.75 + (int)1 = (int)5 + */ + + switch (condition->type) { + case RC_CONDITION_ADD_ADDRESS: + if (condition->oper != RC_OPERAND_NONE) + rc_condition_convert_to_operand(condition, &parse->indirect_parent, parse); + else + memcpy(&parse->indirect_parent, &condition->operand1, sizeof(parse->indirect_parent)); + + break; + + case RC_CONDITION_ADD_SOURCE: + if (parse->addsource_parent.type == RC_OPERAND_NONE) { + rc_condition_convert_to_operand(condition, &parse->addsource_parent, parse); + } + else { + rc_operand_t cond_operand; + /* type determined by parent */ + const uint8_t new_size = rc_operand_is_float(&parse->addsource_parent) ? RC_MEMSIZE_FLOAT : RC_MEMSIZE_32_BITS; + + rc_condition_convert_to_operand(condition, &cond_operand, parse); + rc_operand_addsource(&cond_operand, parse, new_size); + memcpy(&parse->addsource_parent, &cond_operand, sizeof(cond_operand)); + } + + parse->addsource_oper = RC_OPERATOR_ADD; + parse->indirect_parent.type = RC_OPERAND_NONE; + break; + + case RC_CONDITION_SUB_SOURCE: + if (parse->addsource_parent.type == RC_OPERAND_NONE) { + rc_condition_convert_to_operand(condition, &parse->addsource_parent, parse); + parse->addsource_oper = RC_OPERATOR_SUB_PARENT; + } + else { + rc_operand_t cond_operand; + /* type determined by parent */ + const uint8_t new_size = rc_operand_is_float(&parse->addsource_parent) ? RC_MEMSIZE_FLOAT : RC_MEMSIZE_32_BITS; + + if (parse->addsource_oper == RC_OPERATOR_ADD && !rc_operand_is_memref(&parse->addsource_parent)) { + /* if the previous element was a constant we have to turn it into a memref by adding zero */ + rc_modified_memref_t* memref; + rc_operand_t zero; + rc_operand_set_const(&zero, 0); + memref = rc_alloc_modified_memref(parse, + parse->addsource_parent.size, &parse->addsource_parent, RC_OPERATOR_ADD, &zero); + parse->addsource_parent.value.memref = (rc_memref_t*)memref; + parse->addsource_parent.type = RC_OPERAND_ADDRESS; + } + else if (parse->addsource_oper == RC_OPERATOR_SUB_PARENT) { + /* if the previous element was also a SubSource, we have to insert a 0 and start subtracting from there */ + rc_modified_memref_t* negate; + rc_operand_t zero; + + if (rc_operand_is_float(&parse->addsource_parent)) + rc_operand_set_float_const(&zero, 0.0); + else + rc_operand_set_const(&zero, 0); + + negate = rc_alloc_modified_memref(parse, new_size, &parse->addsource_parent, RC_OPERATOR_SUB_PARENT, &zero); + parse->addsource_parent.value.memref = (rc_memref_t*)negate; + parse->addsource_parent.size = zero.size; + } + + /* subtract the condition from the chain */ + parse->addsource_oper = rc_operand_is_memref(&parse->addsource_parent) ? RC_OPERATOR_SUB : RC_OPERATOR_SUB_PARENT; + rc_condition_convert_to_operand(condition, &cond_operand, parse); + rc_operand_addsource(&cond_operand, parse, new_size); + memcpy(&parse->addsource_parent, &cond_operand, sizeof(cond_operand)); + + /* indicate the next value can be added to the chain */ + parse->addsource_oper = RC_OPERATOR_ADD; + } + + parse->indirect_parent.type = RC_OPERAND_NONE; + break; + + case RC_CONDITION_REMEMBER: + rc_condition_convert_to_operand(condition, &condition->operand1, parse); + + if (parse->addsource_parent.type != RC_OPERAND_NONE) { + /* type determined by leaf */ + rc_operand_addsource(&condition->operand1, parse, condition->operand1.size); + condition->operand1.is_combining = 1; + } + + memcpy(&parse->remember, &condition->operand1, sizeof(parse->remember)); + + parse->addsource_parent.type = RC_OPERAND_NONE; + parse->indirect_parent.type = RC_OPERAND_NONE; + break; + + case RC_CONDITION_MEASURED: + /* Measured condition can have modifiers in values */ + if (parse->is_value) { + switch (condition->oper) { + case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: + case RC_OPERATOR_DIV: + case RC_OPERATOR_MULT: + case RC_OPERATOR_MOD: + case RC_OPERATOR_ADD: + case RC_OPERATOR_SUB: + rc_condition_convert_to_operand(condition, &condition->operand1, parse); + break; + + default: + break; + } + } + + /* fallthrough */ /* to default */ + + default: + if (parse->addsource_parent.type != RC_OPERAND_NONE) { + /* type determined by leaf */ + rc_operand_addsource(&condition->operand1, parse, condition->operand1.size); + condition->operand1.is_combining = 1; + + if (parse->buffer) + condition->optimized_comparator = rc_condition_determine_comparator(condition); + } + + parse->addsource_parent.type = RC_OPERAND_NONE; + parse->indirect_parent.type = RC_OPERAND_NONE; + break; + } +} + +static const rc_modified_memref_t* rc_operand_get_modified_memref(const rc_operand_t* operand) { + if (!rc_operand_is_memref(operand)) + return NULL; + + if (operand->value.memref->value.memref_type != RC_MEMREF_TYPE_MODIFIED_MEMREF) + return NULL; + + return (rc_modified_memref_t*)operand->value.memref; +} + +/* rc_condition_update_parse_state will mutate the operand1 to point at the modified memref + * containing the accumulated result up until that point. this function returns the original + * unmodified operand1 from parsing the definition. + */ +const rc_operand_t* rc_condition_get_real_operand1(const rc_condition_t* self) { + const rc_operand_t* operand = &self->operand1; + const rc_modified_memref_t* modified_memref; + + if (operand->is_combining) { + /* operand = "previous + current" - extract current */ + const rc_modified_memref_t* combining_modified_memref = rc_operand_get_modified_memref(operand); + if (combining_modified_memref) + operand = &combining_modified_memref->modifier; + } + + /* modifying operators are merged into an rc_modified_memref_t + * if operand1 is a modified memref, assume it's been merged with the right side and + * extract the parent which is the actual operand1. */ + modified_memref = rc_operand_get_modified_memref(operand); + if (modified_memref) { + if (modified_memref->modifier_type == RC_OPERATOR_INDIRECT_READ) { + /* if the modified memref is an indirect read, the parent is the indirect + * address and the modifier is the offset. the actual size and address are + * stored in the modified memref - use it */ + } else if (rc_operator_is_modifying(self->oper) && self->oper != RC_OPERATOR_NONE) { + /* operand = "parent*modifier" - extract parent.modifier will already be in operand2 */ + operand = &modified_memref->parent; + } + } + + return operand; } int rc_condition_is_combining(const rc_condition_t* self) { @@ -500,41 +697,35 @@ static int rc_test_condition_compare_delta_to_memref_transformed(rc_condition_t* int rc_test_condition(rc_condition_t* self, rc_eval_state_t* eval_state) { rc_typed_value_t value1, value2; - 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; - } + /* 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); @@ -548,38 +739,5 @@ void rc_evaluate_condition_value(rc_typed_value_t* value, rc_condition_t* self, rc_evaluate_operand(value, &self->operand1, eval_state); rc_evaluate_operand(&amount, &self->operand2, eval_state); - switch (self->oper) { - case RC_OPERATOR_MULT: - rc_typed_value_multiply(value, &amount); - break; - - case RC_OPERATOR_DIV: - rc_typed_value_divide(value, &amount); - break; - - case RC_OPERATOR_AND: - 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; - - 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; - - case RC_OPERATOR_MOD: - rc_typed_value_modulus(value, &amount); - break; - - case RC_OPERATOR_ADD: - rc_typed_value_add(value, &amount); - break; - - case RC_OPERATOR_SUB: - rc_typed_value_negate(&amount); - rc_typed_value_add(value, &amount); - break; - } + rc_typed_value_combine(value, &amount, self->oper); } diff --git a/deps/rcheevos/src/rcheevos/condset.c b/deps/rcheevos/src/rcheevos/condset.c index f03d47b452..a76c042dee 100644 --- a/deps/rcheevos/src/rcheevos/condset.c +++ b/deps/rcheevos/src/rcheevos/condset.c @@ -2,272 +2,425 @@ #include /* memcpy */ -static void rc_update_condition_pause(rc_condition_t* condition) { - rc_condition_t* subclause = condition; +enum { + RC_CONDITION_CLASSIFICATION_COMBINING, + RC_CONDITION_CLASSIFICATION_PAUSE, + RC_CONDITION_CLASSIFICATION_RESET, + RC_CONDITION_CLASSIFICATION_HITTARGET, + RC_CONDITION_CLASSIFICATION_MEASURED, + RC_CONDITION_CLASSIFICATION_OTHER, + RC_CONDITION_CLASSIFICATION_INDIRECT +}; - while (condition) { - if (condition->type == RC_CONDITION_PAUSE_IF) { - while (subclause != condition) { - subclause->pause = 1; - subclause = subclause->next; - } - condition->pause = 1; - } - else { - condition->pause = 0; - } +static int rc_classify_condition(const rc_condition_t* cond) { + switch (cond->type) { + case RC_CONDITION_PAUSE_IF: + return RC_CONDITION_CLASSIFICATION_PAUSE; - if (!rc_condition_is_combining(condition)) - subclause = condition->next; + case RC_CONDITION_RESET_IF: + return RC_CONDITION_CLASSIFICATION_RESET; - condition = condition->next; + case RC_CONDITION_ADD_ADDRESS: + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + /* these are handled by rc_modified_memref_t */ + return RC_CONDITION_CLASSIFICATION_INDIRECT; + + case RC_CONDITION_ADD_HITS: + case RC_CONDITION_AND_NEXT: + case RC_CONDITION_OR_NEXT: + case RC_CONDITION_REMEMBER: + case RC_CONDITION_RESET_NEXT_IF: + case RC_CONDITION_SUB_HITS: + return RC_CONDITION_CLASSIFICATION_COMBINING; + + case RC_CONDITION_MEASURED: + case RC_CONDITION_MEASURED_IF: + /* even if not measuring a hit target, we still want to evaluate it every frame */ + return RC_CONDITION_CLASSIFICATION_MEASURED; + + default: + if (cond->required_hits != 0) + return RC_CONDITION_CLASSIFICATION_HITTARGET; + + return RC_CONDITION_CLASSIFICATION_OTHER; } } -rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, int is_value) { - rc_condset_t* self; - rc_condition_t** next; - int in_add_address; - uint32_t measured_target = 0; +static int32_t rc_classify_conditions(rc_condset_t* self, const char* memaddr, const rc_parse_state_t* parent_parse) { + rc_parse_state_t parse; + rc_memrefs_t memrefs; + rc_condition_t condition; + int classification; + uint32_t index = 0; + uint32_t chain_length = 1; - self = RC_ALLOC(rc_condset_t, parse); - self->has_pause = self->is_paused = self->has_indirect_memrefs = 0; - next = &self->conditions; + rc_init_parse_state(&parse, NULL); + rc_init_parse_state_memrefs(&parse, &memrefs); + parse.ignore_non_parse_errors = parent_parse->ignore_non_parse_errors; + + do { + rc_parse_condition_internal(&condition, &memaddr, &parse); + + if (parse.offset < 0) + return parse.offset; + + ++index; + + classification = rc_classify_condition(&condition); + switch (classification) { + case RC_CONDITION_CLASSIFICATION_COMBINING: + ++chain_length; + continue; + + case RC_CONDITION_CLASSIFICATION_INDIRECT: + ++self->num_indirect_conditions; + continue; + + case RC_CONDITION_CLASSIFICATION_PAUSE: + self->num_pause_conditions += chain_length; + break; + + case RC_CONDITION_CLASSIFICATION_RESET: + self->num_reset_conditions += chain_length; + break; + + case RC_CONDITION_CLASSIFICATION_HITTARGET: + self->num_hittarget_conditions += chain_length; + break; + + case RC_CONDITION_CLASSIFICATION_MEASURED: + self->num_measured_conditions += chain_length; + break; + + default: + self->num_other_conditions += chain_length; + break; + } + + chain_length = 1; + } while (*memaddr++ == '_'); + + /* any combining conditions that don't actually feed into a non-combining condition + * need to still have space allocated for them. put them in "other" to match the + * logic in rc_find_next_classification */ + self->num_other_conditions += chain_length - 1; + + return index; +} + +static int rc_find_next_classification(const char* memaddr) { + rc_parse_state_t parse; + rc_memrefs_t memrefs; + rc_condition_t condition; + int classification; + + rc_init_parse_state(&parse, NULL); + rc_init_parse_state_memrefs(&parse, &memrefs); + + do { + rc_parse_condition_internal(&condition, &memaddr, &parse); + if (parse.offset < 0) + break; + + classification = rc_classify_condition(&condition); + switch (classification) { + case RC_CONDITION_CLASSIFICATION_COMBINING: + case RC_CONDITION_CLASSIFICATION_INDIRECT: + break; + + default: + return classification; + } + } while (*memaddr++ == '_'); + + return RC_CONDITION_CLASSIFICATION_OTHER; +} + +static void rc_condition_update_recall_operand(rc_operand_t* operand, const rc_operand_t* remember) +{ + if (operand->type == RC_OPERAND_RECALL) { + if (rc_operand_type_is_memref(operand->memref_access_type) && operand->value.memref == NULL) { + memcpy(operand, remember, sizeof(*remember)); + operand->memref_access_type = operand->type; + operand->type = RC_OPERAND_RECALL; + } + } + else if (rc_operand_is_memref(operand) && operand->value.memref->value.memref_type == RC_MEMREF_TYPE_MODIFIED_MEMREF) { + rc_modified_memref_t* modified_memref = (rc_modified_memref_t*)operand->value.memref; + rc_condition_update_recall_operand(&modified_memref->parent, remember); + rc_condition_update_recall_operand(&modified_memref->modifier, remember); + } +} + +static void rc_update_condition_pause_remember(rc_condset_t* self) { + rc_operand_t* pause_remember = NULL; + rc_condition_t* condition; + rc_condition_t* pause_conditions; + const rc_condition_t* end_pause_condition; + + /* ASSERT: pause conditions are first conditions */ + pause_conditions = rc_condset_get_conditions(self); + end_pause_condition = pause_conditions + self->num_pause_conditions; + + for (condition = pause_conditions; condition < end_pause_condition; ++condition) { + if (condition->type == RC_CONDITION_REMEMBER) { + pause_remember = &condition->operand1; + } + else if (pause_remember == NULL) { + /* if we picked up a non-pause remember, discard it */ + if (condition->operand1.type == RC_OPERAND_RECALL && + rc_operand_type_is_memref(condition->operand1.memref_access_type)) { + condition->operand1.value.memref = NULL; + } + + if (condition->operand2.type == RC_OPERAND_RECALL && + rc_operand_type_is_memref(condition->operand2.memref_access_type)) { + condition->operand2.value.memref = NULL; + } + } + } + + if (pause_remember) { + for (condition = self->conditions; condition; condition = condition->next) { + if (condition >= end_pause_condition) { + /* if we didn't find a remember for a non-pause condition, use the last pause remember */ + rc_condition_update_recall_operand(&condition->operand1, pause_remember); + rc_condition_update_recall_operand(&condition->operand2, pause_remember); + } + + /* Anything after this point will have already been handled */ + if (condition->type == RC_CONDITION_REMEMBER) + break; + } + } +} + +rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse) { + rc_condset_with_trailing_conditions_t* condset_with_conditions; + rc_condset_t local_condset; + rc_condset_t* self; + rc_condition_t condition; + rc_condition_t* conditions; + rc_condition_t** next; + rc_condition_t* pause_conditions = NULL; + rc_condition_t* reset_conditions = NULL; + rc_condition_t* hittarget_conditions = NULL; + rc_condition_t* measured_conditions = NULL; + rc_condition_t* other_conditions = NULL; + rc_condition_t* indirect_conditions = NULL; + int classification, combining_classification = RC_CONDITION_CLASSIFICATION_COMBINING; + uint32_t measured_target = 0; + int32_t result; if (**memaddr == 'S' || **memaddr == 's' || !**memaddr) { /* empty group - editor allows it, so we have to support it */ - *next = 0; + self = RC_ALLOC(rc_condset_t, parse); + memset(self, 0, sizeof(*self)); return self; } - in_add_address = 0; + memset(&local_condset, 0, sizeof(local_condset)); + result = rc_classify_conditions(&local_condset, *memaddr, parse); + if (result < 0) { + parse->offset = result; + return NULL; + } + + condset_with_conditions = RC_ALLOC_WITH_TRAILING(rc_condset_with_trailing_conditions_t, + rc_condition_t, conditions, result, parse); + if (parse->offset < 0) + return NULL; + + self = (rc_condset_t*)condset_with_conditions; + memcpy(self, &local_condset, sizeof(local_condset)); + conditions = &condset_with_conditions->conditions[0]; + + if (parse->buffer) { + pause_conditions = conditions; + conditions += self->num_pause_conditions; + + reset_conditions = conditions; + conditions += self->num_reset_conditions; + + hittarget_conditions = conditions; + conditions += self->num_hittarget_conditions; + + measured_conditions = conditions; + conditions += self->num_measured_conditions; + + other_conditions = conditions; + conditions += self->num_other_conditions; + + indirect_conditions = conditions; + } + + next = &self->conditions; + + /* each condition set has a functionally new recall accumulator */ + parse->remember.type = RC_OPERAND_NONE; + for (;;) { - *next = rc_parse_condition(memaddr, parse, in_add_address); + rc_parse_condition_internal(&condition, memaddr, parse); - if (parse->offset < 0) { - return 0; - } + if (parse->offset < 0) + return NULL; - if ((*next)->oper == RC_OPERATOR_NONE) { - switch ((*next)->type) { + if (condition.oper == RC_OPERATOR_NONE && !parse->ignore_non_parse_errors) { + switch (condition.type) { case RC_CONDITION_ADD_ADDRESS: case RC_CONDITION_ADD_SOURCE: case RC_CONDITION_SUB_SOURCE: case RC_CONDITION_REMEMBER: - /* these conditions don't require a right hand size (implied *1) */ + /* these conditions don't require a right hand side (implied *1) */ break; case RC_CONDITION_MEASURED: /* right hand side is not required when Measured is used in a value */ - if (is_value) + if (parse->is_value) break; /* fallthrough */ /* to default */ default: parse->offset = RC_INVALID_OPERATOR; - return 0; + return NULL; } } - self->has_pause |= (*next)->type == RC_CONDITION_PAUSE_IF; - in_add_address = (*next)->type == RC_CONDITION_ADD_ADDRESS; - self->has_indirect_memrefs |= in_add_address; - - switch ((*next)->type) { - case RC_CONDITION_MEASURED: - if (measured_target != 0) { - /* multiple Measured flags cannot exist in the same group */ - parse->offset = RC_MULTIPLE_MEASURED; - return 0; - } - else if (is_value) { - measured_target = (unsigned)-1; - switch ((*next)->oper) - { - case RC_OPERATOR_AND: - case RC_OPERATOR_XOR: - case RC_OPERATOR_DIV: - case RC_OPERATOR_MULT: - case RC_OPERATOR_MOD: - case RC_OPERATOR_ADD: - case RC_OPERATOR_SUB: - case RC_OPERATOR_NONE: - /* measuring value. leave required_hits at 0 */ - break; - - default: - /* comparison operator, measuring hits. set required_hits to MAX_INT */ - (*next)->required_hits = measured_target; - break; + switch (condition.type) { + case RC_CONDITION_MEASURED: + if (measured_target != 0) { + /* multiple Measured flags cannot exist in the same group */ + if (!parse->ignore_non_parse_errors) { + parse->offset = RC_MULTIPLE_MEASURED; + return NULL; + } } - } - else if ((*next)->required_hits != 0) { - measured_target = (*next)->required_hits; - } - else if ((*next)->operand2.type == RC_OPERAND_CONST) { - measured_target = (*next)->operand2.value.num; - } - else if ((*next)->operand2.type == RC_OPERAND_FP) { - measured_target = (unsigned)(*next)->operand2.value.dbl; + else if (parse->is_value) { + measured_target = (uint32_t)-1; + if (!rc_operator_is_modifying(condition.oper)) { + /* measuring comparison in a value results in a tally (hit count). set target to MAX_INT */ + condition.required_hits = measured_target; + } + } + else if (condition.required_hits != 0) { + measured_target = condition.required_hits; + } + else if (condition.operand2.type == RC_OPERAND_CONST) { + measured_target = condition.operand2.value.num; + } + else if (condition.operand2.type == RC_OPERAND_FP) { + measured_target = (unsigned)condition.operand2.value.dbl; + } + else if (!parse->ignore_non_parse_errors) { + parse->offset = RC_INVALID_MEASURED_TARGET; + return NULL; + } + + if (parse->measured_target && measured_target != parse->measured_target) { + /* multiple Measured flags in separate groups must have the same target */ + if (!parse->ignore_non_parse_errors) { + parse->offset = RC_MULTIPLE_MEASURED; + return NULL; + } + } + + parse->measured_target = measured_target; + break; + + case RC_CONDITION_STANDARD: + case RC_CONDITION_TRIGGER: + /* these flags are not allowed in value expressions */ + if (parse->is_value && !parse->ignore_non_parse_errors) { + parse->offset = RC_INVALID_VALUE_FLAG; + return NULL; + } + break; + } + + rc_condition_update_parse_state(&condition, parse); + + if (parse->buffer) { + classification = rc_classify_condition(&condition); + if (classification == RC_CONDITION_CLASSIFICATION_COMBINING) { + if (combining_classification == RC_CONDITION_CLASSIFICATION_COMBINING) + combining_classification = rc_find_next_classification(&(*memaddr)[1]); /* skip over '_' */ + + classification = combining_classification; } else { - parse->offset = RC_INVALID_MEASURED_TARGET; - return 0; + combining_classification = RC_CONDITION_CLASSIFICATION_COMBINING; } - if (parse->measured_target && measured_target != parse->measured_target) { - /* multiple Measured flags in separate groups must have the same target */ - parse->offset = RC_MULTIPLE_MEASURED; - return 0; + switch (classification) { + case RC_CONDITION_CLASSIFICATION_PAUSE: + memcpy(pause_conditions, &condition, sizeof(condition)); + *next = pause_conditions++; + break; + + case RC_CONDITION_CLASSIFICATION_RESET: + memcpy(reset_conditions, &condition, sizeof(condition)); + *next = reset_conditions++; + break; + + case RC_CONDITION_CLASSIFICATION_HITTARGET: + memcpy(hittarget_conditions, &condition, sizeof(condition)); + *next = hittarget_conditions++; + break; + + case RC_CONDITION_CLASSIFICATION_MEASURED: + memcpy(measured_conditions, &condition, sizeof(condition)); + *next = measured_conditions++; + break; + + case RC_CONDITION_CLASSIFICATION_INDIRECT: + memcpy(indirect_conditions, &condition, sizeof(condition)); + *next = indirect_conditions++; + break; + + default: + memcpy(other_conditions, &condition, sizeof(condition)); + *next = other_conditions++; + break; } - parse->measured_target = measured_target; - break; - - case RC_CONDITION_STANDARD: - case RC_CONDITION_TRIGGER: - /* these flags are not allowed in value expressions */ - if (is_value) { - parse->offset = RC_INVALID_VALUE_FLAG; - return 0; - } - break; - - default: - break; + next = &(*next)->next; } - next = &(*next)->next; - - if (**memaddr != '_') { + if (**memaddr != '_') break; - } (*memaddr)++; } - *next = 0; + *next = NULL; - if (parse->buffer != 0) - rc_update_condition_pause(self->conditions); + self->has_pause = self->num_pause_conditions > 0; + if (self->has_pause && parse->buffer && parse->remember.type != RC_OPERAND_NONE) + rc_update_condition_pause_remember(self); return self; } -static void rc_condset_update_indirect_memrefs(rc_condition_t* condition, int processing_pause, rc_eval_state_t* eval_state) { - for (; condition != 0; condition = condition->next) { - if (condition->pause != processing_pause) - continue; +static uint8_t rc_condset_evaluate_condition_no_add_hits(rc_condition_t* condition, rc_eval_state_t* eval_state) { + /* evaluate the current condition */ + uint8_t cond_valid = (uint8_t)rc_test_condition(condition, eval_state); + condition->is_true = cond_valid; - if (condition->type == RC_CONDITION_ADD_ADDRESS) { - rc_typed_value_t value; - rc_evaluate_condition_value(&value, condition, eval_state); - rc_typed_value_convert(&value, RC_VALUE_TYPE_UNSIGNED); - eval_state->add_address = value.value.u32; - continue; - } + if (eval_state->reset_next) { + /* previous ResetNextIf resets the hit count on this condition and prevents it from being true */ + eval_state->was_cond_reset |= (condition->current_hits != 0); - /* call rc_get_memref_value to update the indirect memrefs. it won't do anything with non-indirect - * memrefs and avoids a second check of is_indirect. also, we ignore the response, so it doesn't - * matter what operand type we pass. assume RC_OPERAND_ADDRESS is the quickest. */ - if (rc_operand_is_memref(&condition->operand1)) - rc_get_memref_value(condition->operand1.value.memref, RC_OPERAND_ADDRESS, eval_state); - - if (rc_operand_is_memref(&condition->operand2)) - rc_get_memref_value(condition->operand2.value.memref, RC_OPERAND_ADDRESS, eval_state); - - eval_state->add_address = 0; + condition->current_hits = 0; + cond_valid = 0; } -} + else { + /* apply chained logic flags */ + cond_valid &= eval_state->and_next; + cond_valid |= eval_state->or_next; -static int rc_test_condset_internal(rc_condset_t* self, int processing_pause, rc_eval_state_t* eval_state) { - rc_condition_t* condition; - rc_typed_value_t value; - int set_valid, cond_valid, and_next, or_next, reset_next, measured_from_hits, can_measure; - rc_typed_value_t measured_value; - uint32_t total_hits; - - measured_value.type = RC_VALUE_TYPE_NONE; - measured_from_hits = 0; - can_measure = 1; - total_hits = 0; - - eval_state->primed = 1; - set_valid = 1; - and_next = 1; - or_next = 0; - reset_next = 0; - eval_state->add_value.type = RC_VALUE_TYPE_NONE; - eval_state->add_hits = eval_state->add_address = 0; - - for (condition = self->conditions; condition != 0; condition = condition->next) { - if (condition->pause != processing_pause) - continue; - - /* STEP 1: process modifier conditions */ - switch (condition->type) { - case RC_CONDITION_ADD_SOURCE: - rc_evaluate_condition_value(&value, condition, eval_state); - rc_typed_value_add(&eval_state->add_value, &value); - eval_state->add_address = 0; - continue; - - case RC_CONDITION_SUB_SOURCE: - rc_evaluate_condition_value(&value, condition, eval_state); - rc_typed_value_negate(&value); - rc_typed_value_add(&eval_state->add_value, &value); - eval_state->add_address = 0; - continue; - - case RC_CONDITION_ADD_ADDRESS: - rc_evaluate_condition_value(&value, condition, eval_state); - rc_typed_value_convert(&value, RC_VALUE_TYPE_UNSIGNED); - eval_state->add_address = value.value.u32; - continue; - - case RC_CONDITION_REMEMBER: - rc_evaluate_condition_value(&value, condition, eval_state); - rc_typed_value_add(&value, &eval_state->add_value); - eval_state->recall_value.type = value.type; - eval_state->recall_value.value = value.value; - eval_state->add_value.type = RC_VALUE_TYPE_NONE; - eval_state->add_address = 0; - continue; - - case RC_CONDITION_MEASURED: - if (condition->required_hits == 0 && can_measure) { - /* Measured condition without a hit target measures the value of the left operand */ - rc_evaluate_condition_value(&measured_value, condition, eval_state); - rc_typed_value_add(&measured_value, &eval_state->add_value); - } - break; - - default: - break; - } - - /* STEP 2: evaluate the current condition */ - condition->is_true = (char)rc_test_condition(condition, eval_state); - eval_state->add_value.type = RC_VALUE_TYPE_NONE; - eval_state->add_address = 0; - - /* apply logic flags and reset them for the next condition */ - cond_valid = condition->is_true; - cond_valid &= and_next; - cond_valid |= or_next; - and_next = 1; - or_next = 0; - - if (reset_next) { - /* previous ResetNextIf resets the hit count on this condition and prevents it from being true */ - if (condition->current_hits) - eval_state->was_cond_reset = 1; - - condition->current_hits = 0; - cond_valid = 0; - } - else if (cond_valid) { - /* true conditions should update hit count */ + if (cond_valid) { + /* true conditions should update their hit count */ eval_state->has_hits = 1; if (condition->required_hits == 0) { @@ -288,161 +441,316 @@ static int rc_test_condset_internal(rc_condset_t* self, int processing_pause, rc eval_state->has_hits = 1; cond_valid = (condition->current_hits == condition->required_hits); } + } - /* STEP 3: handle logic flags */ + /* reset chained logic flags for the next condition */ + eval_state->and_next = 1; + eval_state->or_next = 0; + + return cond_valid; +} + +static uint32_t rc_condset_evaluate_total_hits(rc_condition_t* condition, rc_eval_state_t* eval_state) { + uint32_t total_hits = condition->current_hits; + + if (condition->required_hits != 0) { + /* if the condition has a target hit count, we have to recalculate cond_valid including the AddHits counter */ + const int32_t signed_hits = (int32_t)condition->current_hits + eval_state->add_hits; + total_hits = (signed_hits >= 0) ? (uint32_t)signed_hits : 0; + } + else { + /* no target hit count. we can't tell if the add_hits value is from this frame or not, so ignore it. + complex condition will only be true if the current condition is true */ + } + + eval_state->add_hits = 0; + + return total_hits; +} + +static uint8_t rc_condset_evaluate_condition(rc_condition_t* condition, rc_eval_state_t* eval_state) { + uint8_t cond_valid = rc_condset_evaluate_condition_no_add_hits(condition, eval_state); + + if (eval_state->add_hits != 0 && condition->required_hits != 0) { + uint32_t total_hits = rc_condset_evaluate_total_hits(condition, eval_state); + cond_valid = (total_hits >= condition->required_hits); + } + + /* reset logic flags for the next condition */ + eval_state->reset_next = 0; + + return cond_valid; +} + +static void rc_condset_evaluate_standard(rc_condition_t* condition, rc_eval_state_t* eval_state) { + const uint8_t cond_valid = rc_condset_evaluate_condition(condition, eval_state); + + eval_state->is_true &= cond_valid; + eval_state->is_primed &= cond_valid; + + if (!cond_valid && eval_state->can_short_curcuit) + eval_state->stop_processing = 1; +} + +static void rc_condset_evaluate_pause_if(rc_condition_t* condition, rc_eval_state_t* eval_state) { + const uint8_t cond_valid = rc_condset_evaluate_condition(condition, eval_state); + + if (cond_valid) { + eval_state->is_paused = 1; + + /* set cannot be valid if it's paused */ + eval_state->is_true = eval_state->is_primed = 0; + + /* as soon as we find a PauseIf that evaluates to true, stop processing the rest of the group */ + eval_state->stop_processing = 1; + } + else if (condition->required_hits == 0) { + /* PauseIf didn't evaluate true, and doesn't have a HitCount, reset the HitCount to indicate the condition didn't match */ + condition->current_hits = 0; + } + else { + /* PauseIf has a HitCount that hasn't been met, ignore it for now. */ + } +} + +static void rc_condset_evaluate_reset_if(rc_condition_t* condition, rc_eval_state_t* eval_state) { + const uint8_t cond_valid = rc_condset_evaluate_condition(condition, eval_state); + + if (cond_valid) { + /* flag the condition as being responsible for the reset */ + /* make sure not to modify bit0, as we use bitwise-and operators to combine truthiness */ + condition->is_true |= 0x02; + + /* set cannot be valid if we've hit a reset condition */ + eval_state->is_true = eval_state->is_primed = 0; + + /* let caller know to reset all hit counts */ + eval_state->was_reset = 1; + + /* can stop processing once an active ResetIf is encountered */ + eval_state->stop_processing = 1; + } +} + +static void rc_condset_evaluate_trigger(rc_condition_t* condition, rc_eval_state_t* eval_state) { + const uint8_t cond_valid = rc_condset_evaluate_condition(condition, eval_state); + + eval_state->is_true &= cond_valid; +} + +static void rc_condset_evaluate_measured(rc_condition_t* condition, rc_eval_state_t* eval_state) { + if (condition->required_hits == 0) { + rc_condset_evaluate_standard(condition, eval_state); + + /* Measured condition without a hit target measures the value of the left operand */ + rc_evaluate_operand(&eval_state->measured_value, &condition->operand1, eval_state); + eval_state->measured_from_hits = 0; + } + else { + /* this largely mimicks rc_condset_evaluate_condition, but captures the total_hits */ + uint8_t cond_valid = rc_condset_evaluate_condition_no_add_hits(condition, eval_state); + const uint32_t total_hits = rc_condset_evaluate_total_hits(condition, eval_state); + + cond_valid = (total_hits >= condition->required_hits); + eval_state->is_true &= cond_valid; + eval_state->is_primed &= cond_valid; + + /* if there is a hit target, capture the current hits */ + eval_state->measured_value.value.u32 = total_hits; + eval_state->measured_value.type = RC_VALUE_TYPE_UNSIGNED; + eval_state->measured_from_hits = 1; + + /* reset logic flags for the next condition */ + eval_state->reset_next = 0; + } +} + +static void rc_condset_evaluate_measured_if(rc_condition_t* condition, rc_eval_state_t* eval_state) { + const uint8_t cond_valid = rc_condset_evaluate_condition(condition, eval_state); + + eval_state->is_true &= cond_valid; + eval_state->is_primed &= cond_valid; + eval_state->can_measure &= cond_valid; +} + +static void rc_condset_evaluate_add_hits(rc_condition_t* condition, rc_eval_state_t* eval_state) { + rc_condset_evaluate_condition_no_add_hits(condition, eval_state); + + eval_state->add_hits += (int32_t)condition->current_hits; + + /* ResetNextIf was applied to this AddHits condition; don't apply it to future conditions */ + eval_state->reset_next = 0; +} + +static void rc_condset_evaluate_sub_hits(rc_condition_t* condition, rc_eval_state_t* eval_state) { + rc_condset_evaluate_condition_no_add_hits(condition, eval_state); + + eval_state->add_hits -= (int32_t)condition->current_hits; + + /* ResetNextIf was applied to this AddHits condition; don't apply it to future conditions */ + eval_state->reset_next = 0; +} + +static void rc_condset_evaluate_reset_next_if(rc_condition_t* condition, rc_eval_state_t* eval_state) { + eval_state->reset_next = rc_condset_evaluate_condition_no_add_hits(condition, eval_state); +} + +static void rc_condset_evaluate_and_next(rc_condition_t* condition, rc_eval_state_t* eval_state) { + eval_state->and_next = rc_condset_evaluate_condition_no_add_hits(condition, eval_state); +} + +static void rc_condset_evaluate_or_next(rc_condition_t* condition, rc_eval_state_t* eval_state) { + eval_state->or_next = rc_condset_evaluate_condition_no_add_hits(condition, eval_state); +} + +static void rc_test_condset_internal(rc_condition_t* condition, uint32_t num_conditions, + rc_eval_state_t* eval_state, int can_short_circuit) { + const rc_condition_t* condition_end = condition + num_conditions; + for (; condition < condition_end; ++condition) { switch (condition->type) { - case RC_CONDITION_ADD_HITS: - eval_state->add_hits += condition->current_hits; - reset_next = 0; /* ResetNextIf was applied to this AddHits condition; don't apply it to future conditions */ - continue; - - case RC_CONDITION_SUB_HITS: - eval_state->add_hits -= condition->current_hits; - reset_next = 0; /* ResetNextIf was applied to this AddHits condition; don't apply it to future conditions */ - continue; - - case RC_CONDITION_RESET_NEXT_IF: - reset_next = cond_valid; - continue; - - case RC_CONDITION_AND_NEXT: - and_next = cond_valid; - continue; - - case RC_CONDITION_OR_NEXT: - or_next = cond_valid; - continue; - - default: + case RC_CONDITION_STANDARD: + rc_condset_evaluate_standard(condition, eval_state); break; - } - - /* reset logic flags for next condition */ - reset_next = 0; - - /* STEP 4: calculate total hits */ - total_hits = condition->current_hits; - - if (eval_state->add_hits) { - if (condition->required_hits != 0) { - /* if the condition has a target hit count, we have to recalculate cond_valid including the AddHits counter */ - const int signed_hits = (int)condition->current_hits + eval_state->add_hits; - total_hits = (signed_hits >= 0) ? (unsigned)signed_hits : 0; - cond_valid = (total_hits >= condition->required_hits); - } - else { - /* no target hit count. we can't tell if the add_hits value is from this frame or not, so ignore it. - complex condition will only be true if the current condition is true */ - } - - eval_state->add_hits = 0; - } - - /* STEP 5: handle special flags */ - switch (condition->type) { case RC_CONDITION_PAUSE_IF: - /* as soon as we find a PauseIf that evaluates to true, stop processing the rest of the group */ - if (cond_valid) { - /* indirect memrefs are not updated as part of the rc_update_memref_values call. - * an active pause aborts processing of the remaining part of the pause subset and the entire non-pause subset. - * if the set has any indirect memrefs, manually update them now so the deltas are correct */ - if (self->has_indirect_memrefs) { - /* first, update any indirect memrefs in the remaining part of the pause subset */ - rc_condset_update_indirect_memrefs(condition->next, 1, eval_state); - - /* then, update all indirect memrefs in the non-pause subset */ - rc_condset_update_indirect_memrefs(self->conditions, 0, eval_state); - } - - return 1; - } - - /* if we make it to the end of the function, make sure we indicate that nothing matched. if we do find - a later PauseIf match, it'll automatically return true via the previous condition. */ - set_valid = 0; - - if (condition->required_hits == 0) { - /* PauseIf didn't evaluate true, and doesn't have a HitCount, reset the HitCount to indicate the condition didn't match */ - condition->current_hits = 0; - } - else { - /* PauseIf has a HitCount that hasn't been met, ignore it for now. */ - } - - continue; - + rc_condset_evaluate_pause_if(condition, eval_state); + break; case RC_CONDITION_RESET_IF: - if (cond_valid) { - eval_state->was_reset = 1; /* let caller know to reset all hit counts */ - set_valid = 0; /* cannot be valid if we've hit a reset condition */ - } - continue; - - case RC_CONDITION_MEASURED: - if (condition->required_hits != 0) { - /* if there's a hit target, capture the current hits for recording Measured value later */ - measured_from_hits = 1; - if (can_measure) { - measured_value.value.u32 = total_hits; - measured_value.type = RC_VALUE_TYPE_UNSIGNED; - } - } + rc_condset_evaluate_reset_if(condition, eval_state); break; - - case RC_CONDITION_MEASURED_IF: - if (!cond_valid) { - measured_value.value.u32 = 0; - measured_value.type = RC_VALUE_TYPE_UNSIGNED; - can_measure = 0; - } - break; - case RC_CONDITION_TRIGGER: - /* update truthiness of set, but do not update truthiness of primed state */ - set_valid &= cond_valid; - continue; - + rc_condset_evaluate_trigger(condition, eval_state); + break; + case RC_CONDITION_MEASURED: + rc_condset_evaluate_measured(condition, eval_state); + break; + case RC_CONDITION_MEASURED_IF: + rc_condset_evaluate_measured_if(condition, eval_state); + break; + case RC_CONDITION_ADD_SOURCE: + case RC_CONDITION_SUB_SOURCE: + case RC_CONDITION_ADD_ADDRESS: + case RC_CONDITION_REMEMBER: + /* these are handled by rc_modified_memref_t */ + break; + case RC_CONDITION_ADD_HITS: + rc_condset_evaluate_add_hits(condition, eval_state); + break; + case RC_CONDITION_SUB_HITS: + rc_condset_evaluate_sub_hits(condition, eval_state); + break; + case RC_CONDITION_RESET_NEXT_IF: + rc_condset_evaluate_reset_next_if(condition, eval_state); + break; + case RC_CONDITION_AND_NEXT: + rc_condset_evaluate_and_next(condition, eval_state); + break; + case RC_CONDITION_OR_NEXT: + rc_condset_evaluate_or_next(condition, eval_state); + break; default: + eval_state->stop_processing = 1; + eval_state->is_true = eval_state->is_primed = 0; break; } - /* STEP 5: update overall truthiness of set and primed state */ - eval_state->primed &= cond_valid; - set_valid &= cond_valid; + if (eval_state->stop_processing && can_short_circuit) + break; } +} - if (measured_value.type != RC_VALUE_TYPE_NONE) { - /* if no previous Measured value was captured, or the new one is greater, keep the new one */ - if (eval_state->measured_value.type == RC_VALUE_TYPE_NONE || - rc_typed_value_compare(&measured_value, &eval_state->measured_value, RC_OPERATOR_GT)) { - memcpy(&eval_state->measured_value, &measured_value, sizeof(measured_value)); - eval_state->measured_from_hits = (char)measured_from_hits; - } - } +rc_condition_t* rc_condset_get_conditions(rc_condset_t* self) { + if (self->conditions) + return RC_GET_TRAILING(self, rc_condset_with_trailing_conditions_t, rc_condition_t, conditions); - return set_valid; + return NULL; } int rc_test_condset(rc_condset_t* self, rc_eval_state_t* eval_state) { - if (self->conditions == 0) { - /* important: empty group must evaluate true */ - return 1; + rc_condition_t* conditions; + + /* reset the processing state before processing each condset. do not reset the result state. */ + eval_state->measured_value.type = RC_VALUE_TYPE_NONE; + eval_state->add_hits = 0; + eval_state->is_true = 1; + eval_state->is_primed = 1; + eval_state->is_paused = 0; + eval_state->can_measure = 1; + eval_state->measured_from_hits = 0; + eval_state->and_next = 1; + eval_state->or_next = 0; + eval_state->reset_next = 0; + eval_state->stop_processing = 0; + + /* the conditions array is allocated immediately after the rc_condset_t, without a separate pointer */ + conditions = rc_condset_get_conditions(self); + + if (self->num_pause_conditions) { + /* one or more Pause conditions exist. if any of them are true (eval_state->is_paused), + * stop processing this group */ + rc_test_condset_internal(conditions, self->num_pause_conditions, eval_state, 1); + + self->is_paused = eval_state->is_paused; + if (self->is_paused) { + /* condset is paused. stop processing immediately. */ + return 0; + } + + conditions += self->num_pause_conditions; } - /* initialize recall value so each condition set has a functionally new recall accumulator */ - eval_state->recall_value.type = RC_VALUE_TYPE_UNSIGNED; - eval_state->recall_value.value.u32 = 0; + if (self->num_reset_conditions) { + /* one or more Reset conditions exists. if any of them are true (eval_state->was_reset), + * we'll skip some of the later steps */ + rc_test_condset_internal(conditions, self->num_reset_conditions, eval_state, eval_state->can_short_curcuit); + conditions += self->num_reset_conditions; + } - if (self->has_pause) { - /* one or more Pause conditions exists, if any of them are true, stop processing this group */ - self->is_paused = (char)rc_test_condset_internal(self, 1, eval_state); - if (self->is_paused) { - eval_state->primed = 0; - return 0; + if (self->num_hittarget_conditions) { + /* one or more hit target conditions exists. these must be processed every frame, + * unless their hit count is going to be reset */ + if (!eval_state->was_reset) + rc_test_condset_internal(conditions, self->num_hittarget_conditions, eval_state, 0); + + conditions += self->num_hittarget_conditions; + } + + if (self->num_measured_conditions) { + /* IMPORTANT: reset hit counts on these conditions before processing them so + * the MeasuredIf logic and Measured value are correct. + * NOTE: a ResetIf in a later alt group may not have been processed yet. + * Accept that as a weird edge case, and just recommend the user + * move the ResetIf if it becomes a problem. */ + if (eval_state->was_reset) { + int i; + for (i = 0; i < self->num_measured_conditions; ++i) + conditions[i].current_hits = 0; + } + + /* the measured value must be calculated every frame, even if hit counts will be reset */ + rc_test_condset_internal(conditions, self->num_measured_conditions, eval_state, 0); + conditions += self->num_measured_conditions; + + if (eval_state->measured_value.type != RC_VALUE_TYPE_NONE) { + /* if a MeasuredIf was false (!eval_state->can_measure), or the measured + * value is a hitcount and a ResetIf is true, zero out the measured value */ + if (!eval_state->can_measure || + (eval_state->measured_from_hits && eval_state->was_reset)) { + eval_state->measured_value.type = RC_VALUE_TYPE_UNSIGNED; + eval_state->measured_value.value.u32 = 0; + } } } - return rc_test_condset_internal(self, 0, eval_state); + if (self->num_other_conditions) { + /* the remaining conditions only need to be evaluated if the rest of the condset is true */ + if (eval_state->is_true) + rc_test_condset_internal(conditions, self->num_other_conditions, eval_state, eval_state->can_short_curcuit); + /* something else is false. if we can't short circuit, and there wasn't a reset, we still need to evaluate these */ + else if (!eval_state->can_short_curcuit && !eval_state->was_reset) + rc_test_condset_internal(conditions, self->num_other_conditions, eval_state, eval_state->can_short_curcuit); + } + + return eval_state->is_true; } void rc_reset_condset(rc_condset_t* self) { diff --git a/deps/rcheevos/src/rcheevos/consoleinfo.c b/deps/rcheevos/src/rcheevos/consoleinfo.c index 98b43b6d04..9277e9bad2 100644 --- a/deps/rcheevos/src/rcheevos/consoleinfo.c +++ b/deps/rcheevos/src/rcheevos/consoleinfo.c @@ -72,6 +72,9 @@ const char* rc_console_name(uint32_t console_id) case RC_CONSOLE_FAIRCHILD_CHANNEL_F: return "Fairchild Channel F"; + case RC_CONSOLE_FAMICOM_DISK_SYSTEM: + return "Famicom Disk System"; + case RC_CONSOLE_FM_TOWNS: return "FM Towns"; @@ -380,14 +383,18 @@ static const rc_memory_regions_t rc_memory_regions_colecovision = { _rc_memory_r /* ===== Commodore 64 ===== */ /* https://www.c64-wiki.com/wiki/Memory_Map */ /* https://sta.c64.org/cbm64mem.html */ +/* NOTE: Several blocks of C64 memory can be bank-switched for ROM data (see https://www.c64-wiki.com/wiki/Bank_Switching). + * Achievement triggers rely on values changing, so we don't really need to look at the ROM data. + * The achievement logic assumes the RAM data is always present in the bankable blocks. As such, + * clients providing memory to achievements should always return the RAM values at the queried address. */ static const rc_memory_region_t _rc_memory_regions_c64[] = { { 0x000000U, 0x0003FFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, { 0x000400U, 0x0007FFU, 0x000400U, RC_MEMORY_TYPE_VIDEO_RAM, "Screen RAM" }, - { 0x000800U, 0x009FFFU, 0x000800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* BASIC Program Storage Area */ - { 0x00A000U, 0x00BFFFU, 0x00A000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Machine Language Storage Area / BASIC ROM Area */ - { 0x00C000U, 0x00CFFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Machine Language Storage Area */ - { 0x00D000U, 0x00DFFFU, 0x00D000U, RC_MEMORY_TYPE_SYSTEM_RAM, "I/O Area" }, /* also Character ROM */ - { 0x00E000U, 0x00FFFFU, 0x00E000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* Machine Language Storage Area / Kernal ROM */ + { 0x000800U, 0x009FFFU, 0x000800U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* BASIC area. $8000-$9FFF can bank to cartridge ROM */ + { 0x00A000U, 0x00BFFFU, 0x00A000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* can bank to BASIC ROM or cartridge ROM */ + { 0x00C000U, 0x00CFFFU, 0x00C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x00D000U, 0x00DFFFU, 0x00D000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* can bank to I/O Area or character ROM */ + { 0x00E000U, 0x00FFFFU, 0x00E000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* can bank to kernel ROM */ }; static const rc_memory_regions_t rc_memory_regions_c64 = { _rc_memory_regions_c64, 7 }; @@ -423,6 +430,24 @@ static const rc_memory_region_t _rc_memory_regions_fairchild_channel_f[] = { }; static const rc_memory_regions_t rc_memory_regions_fairchild_channel_f = { _rc_memory_regions_fairchild_channel_f, 4 }; +/* ===== Famicon Disk System ===== */ +/* https://fms.komkon.org/EMUL8/NES.html */ +static const rc_memory_region_t _rc_memory_regions_famicom_disk_system[] = { + { 0x0000U, 0x07FFU, 0x0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x0800U, 0x0FFFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x1000U, 0x17FFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x1800U, 0x1FFFU, 0x0000U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirror RAM" }, /* duplicates memory from $0000-$07FF */ + { 0x2000U, 0x2007U, 0x2000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "PPU Register" }, + { 0x2008U, 0x3FFFU, 0x2008U, RC_MEMORY_TYPE_VIRTUAL_RAM, "Mirrored PPU Register" }, /* repeats every 8 bytes */ + { 0x4000U, 0x4017U, 0x4000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "APU and I/O register" }, + { 0x4018U, 0x401FU, 0x4018U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "APU and I/O test register" }, + { 0x4020U, 0x40FFU, 0x4020U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "FDS I/O registers"}, + { 0x4100U, 0x5FFFU, 0x4100U, RC_MEMORY_TYPE_READONLY, "Cartridge data"}, /* varies by mapper */ + { 0x6000U, 0xDFFFU, 0x6000U, RC_MEMORY_TYPE_SYSTEM_RAM, "FDS RAM"}, + { 0xE000U, 0xFFFFU, 0xE000U, RC_MEMORY_TYPE_READONLY, "FDS BIOS ROM"}, +}; +static const rc_memory_regions_t rc_memory_regions_famicom_disk_system = { _rc_memory_regions_famicom_disk_system, 12 }; + /* ===== GameBoy / MegaDuck ===== */ static const rc_memory_region_t _rc_memory_regions_gameboy[] = { { 0x000000U, 0x0000FFU, 0x000000U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "Interrupt vector" }, @@ -694,27 +719,16 @@ static const rc_memory_region_t _rc_memory_regions_nes[] = { { 0x4020U, 0x5FFFU, 0x4020U, RC_MEMORY_TYPE_READONLY, "Cartridge data"}, /* varies by mapper */ { 0x6000U, 0x7FFFU, 0x6000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM"}, { 0x8000U, 0xFFFFU, 0x8000U, RC_MEMORY_TYPE_READONLY, "Cartridge ROM"}, - - /* NOTE: these are the correct mappings for FDS: https://fms.komkon.org/EMUL8/NES.html - * 0x6000-0xDFFF is RAM on the FDS system and 0xE000-0xFFFF is FDS BIOS. - * If the core implements a memory map, we should still be able to translate the addresses - * correctly as we only use the classifications when a memory map is not provided - - { 0x4020U, 0x40FFU, 0x4020U, RC_MEMORY_TYPE_HARDWARE_CONTROLLER, "FDS I/O registers"}, - { 0x4100U, 0x5FFFU, 0x4100U, RC_MEMORY_TYPE_READONLY, "Cartridge data"}, // varies by mapper - { 0x6000U, 0xDFFFU, 0x6000U, RC_MEMORY_TYPE_SYSTEM_RAM, "FDS RAM"}, - { 0xE000U, 0xFFFFU, 0xE000U, RC_MEMORY_TYPE_READONLY, "FDS BIOS ROM"}, - - */ }; static const rc_memory_regions_t rc_memory_regions_nes = { _rc_memory_regions_nes, 11 }; /* ===== Nintendo 64 ===== */ /* https://raw.githubusercontent.com/mikeryan/n64dev/master/docs/n64ops/n64ops%23h.txt */ +/* https://n64brew.dev/wiki/Memory_map#Virtual_Memory_Map */ static const rc_memory_region_t _rc_memory_regions_n64[] = { - { 0x000000U, 0x1FFFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 1 */ - { 0x200000U, 0x3FFFFFU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 2 */ - { 0x400000U, 0x7FFFFFU, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } /* expansion pak - cannot find any details for real address */ + { 0x000000U, 0x1FFFFFU, 0x80000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 1 */ + { 0x200000U, 0x3FFFFFU, 0x80200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RDRAM 2 */ + { 0x400000U, 0x7FFFFFU, 0x80400000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } /* expansion pak */ }; static const rc_memory_regions_t rc_memory_regions_n64 = { _rc_memory_regions_n64, 3 }; @@ -820,12 +834,13 @@ static const rc_memory_region_t _rc_memory_regions_pokemini[] = { static const rc_memory_regions_t rc_memory_regions_pokemini = { _rc_memory_regions_pokemini, 2 }; /* ===== Sega CD ===== */ -/* https://en.wikibooks.org/wiki/Genesis_Programming#MegaCD_Changes */ +/* https://en.wikibooks.org/wiki/Genesis_Programming/68K_Memory_map/ */ static const rc_memory_region_t _rc_memory_regions_segacd[] = { { 0x000000U, 0x00FFFFU, 0x00FF0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "68000 RAM" }, - { 0x010000U, 0x08FFFFU, 0x80020000U, RC_MEMORY_TYPE_SAVE_RAM, "CD PRG RAM" } /* normally banked into $020000-$03FFFF */ + { 0x010000U, 0x08FFFFU, 0x80020000U, RC_MEMORY_TYPE_SYSTEM_RAM, "CD PRG RAM" }, /* normally banked into $020000-$03FFFF */ + { 0x090000U, 0x0AFFFFU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "CD WORD RAM" } }; -static const rc_memory_regions_t rc_memory_regions_segacd = { _rc_memory_regions_segacd, 2 }; +static const rc_memory_regions_t rc_memory_regions_segacd = { _rc_memory_regions_segacd, 3 }; /* ===== Sega Saturn ===== */ /* https://segaretro.org/Sega_Saturn_hardware_notes_(2004-04-27) */ @@ -868,10 +883,18 @@ static const rc_memory_regions_t rc_memory_regions_scv = { _rc_memory_regions_sc /* ===== Super Nintendo ===== */ /* https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map#LoROM */ static const rc_memory_region_t _rc_memory_regions_snes[] = { - { 0x000000U, 0x01FFFFU, 0x7E0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, - { 0x020000U, 0x03FFFFU, 0xFE0000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" } + { 0x000000U, 0x01FFFFU, 0x07E0000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* Cartridge RAM here could be in a variety of places in SNES memory, depending on the ROM type. + * Due to this, we place Cartridge RAM outside of the possible native addressing space. + * Note that this also covers SA-1 BW-RAM (which is exposed as RETRO_MEMORY_SAVE_RAM for libretro). + */ + { 0x020000U, 0x09FFFFU, 0x1000000U, RC_MEMORY_TYPE_SAVE_RAM, "Cartridge RAM" }, + /* I-RAM on the SA-1 is normally at 0x003000. However, this address typically just has a mirror of System RAM for other ROM types. + * To avoid conflicts, don't use 0x003000, instead map it outside of the possible native addressing space. + */ + { 0x0A0000U, 0x0A07FFU, 0x1080000U, RC_MEMORY_TYPE_SYSTEM_RAM, "I-RAM (SA-1)" } }; -static const rc_memory_regions_t rc_memory_regions_snes = { _rc_memory_regions_snes, 2 }; +static const rc_memory_regions_t rc_memory_regions_snes = { _rc_memory_regions_snes, 3 }; /* ===== Thomson TO8 ===== */ /* https://github.com/mamedev/mame/blob/master/src/mame/drivers/thomson.cpp#L1617 */ @@ -1046,7 +1069,10 @@ const rc_memory_regions_t* rc_console_memory_regions(uint32_t console_id) case RC_CONSOLE_FAIRCHILD_CHANNEL_F: return &rc_memory_regions_fairchild_channel_f; - + + case RC_CONSOLE_FAMICOM_DISK_SYSTEM: + return &rc_memory_regions_famicom_disk_system; + case RC_CONSOLE_GAMEBOY: return &rc_memory_regions_gameboy; diff --git a/deps/rcheevos/src/rcheevos/format.c b/deps/rcheevos/src/rcheevos/format.c index 0aa44ee6a1..bfeadf44cf 100644 --- a/deps/rcheevos/src/rcheevos/format.c +++ b/deps/rcheevos/src/rcheevos/format.c @@ -4,6 +4,7 @@ #include #include +#include int rc_parse_format(const char* format_str) { switch (*format_str++) { @@ -158,6 +159,52 @@ static int rc_format_value_padded(char* buffer, size_t size, const char* format, return snprintf(buffer, size, format, value); } +static int rc_format_insert_commas(int chars, char* buffer, size_t size) +{ + int to_insert; + char* src = buffer; + char* ptr; + char* dst = &buffer[chars]; + if (chars == 0) + return 0; + + /* ignore leading negative sign */ + if (*src == '-') + src++; + + /* determine how many digits are present in the leading number */ + ptr = src; + while (ptr < dst && isdigit((int)*ptr)) + ++ptr; + + /* determine how many commas are needed */ + to_insert = (int)((ptr - src - 1) / 3); + if (to_insert == 0) /* no commas needed */ + return chars; + + /* if there's not enough room to insert the commas, leave string as-is, but return wanted space */ + chars += to_insert; + if (chars >= (int)size) + return chars; + + /* move the trailing part of the string */ + memmove(ptr + to_insert, ptr, dst - ptr + 1); + + /* shift blocks of three digits at a time, inserting commas in front of them */ + src = ptr - 1; + dst = src + to_insert; + while (to_insert > 0) { + *dst-- = *src--; + *dst-- = *src--; + *dst-- = *src--; + *dst-- = ','; + + --to_insert; + } + + return chars; +} + int rc_format_typed_value(char* buffer, size_t size, const rc_typed_value_t* value, int format) { int chars; rc_typed_value_t converted_value; @@ -199,8 +246,7 @@ int rc_format_typed_value(char* buffer, size_t size, const rc_typed_value_t* val case RC_FORMAT_SCORE: rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_SIGNED); - chars = snprintf(buffer, size, "%06d", converted_value.value.i32); - break; + return snprintf(buffer, size, "%06d", converted_value.value.i32); case RC_FORMAT_FLOAT1: rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_FLOAT); @@ -266,9 +312,13 @@ int rc_format_typed_value(char* buffer, size_t size, const rc_typed_value_t* val rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); chars = snprintf(buffer, size, "%u", converted_value.value.u32); break; + + case RC_FORMAT_UNFORMATTED: + rc_typed_value_convert(&converted_value, RC_VALUE_TYPE_UNSIGNED); + return snprintf(buffer, size, "%u", converted_value.value.u32); } - return chars; + return rc_format_insert_commas(chars, buffer, size); } int rc_format_value(char* buffer, int size, int32_t value, int format) { diff --git a/deps/rcheevos/src/rcheevos/lboard.c b/deps/rcheevos/src/rcheevos/lboard.c index 47e05f2738..84ff5460af 100644 --- a/deps/rcheevos/src/rcheevos/lboard.c +++ b/deps/rcheevos/src/rcheevos/lboard.c @@ -29,7 +29,6 @@ void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_s if (*memaddr && *memaddr != ':') { found |= RC_LBOARD_START; rc_parse_trigger_internal(&self->start, &memaddr, parse); - self->start.memrefs = 0; } } else if ((memaddr[0] == 'c' || memaddr[0] == 'C') && @@ -44,7 +43,6 @@ void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_s if (*memaddr && *memaddr != ':') { found |= RC_LBOARD_CANCEL; rc_parse_trigger_internal(&self->cancel, &memaddr, parse); - self->cancel.memrefs = 0; } } else if ((memaddr[0] == 's' || memaddr[0] == 'S') && @@ -59,7 +57,6 @@ void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_s if (*memaddr && *memaddr != ':') { found |= RC_LBOARD_SUBMIT; rc_parse_trigger_internal(&self->submit, &memaddr, parse); - self->submit.memrefs = 0; } } else if ((memaddr[0] == 'v' || memaddr[0] == 'V') && @@ -74,7 +71,6 @@ void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_s if (*memaddr && *memaddr != ':') { found |= RC_LBOARD_VALUE; rc_parse_value_internal(&self->value, &memaddr, parse); - self->value.memrefs = 0; } } else if ((memaddr[0] == 'p' || memaddr[0] == 'P') && @@ -91,7 +87,6 @@ void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_s self->progress = RC_ALLOC(rc_value_t, parse); rc_parse_value_internal(self->progress, &memaddr, parse); - self->progress->memrefs = 0; } } @@ -130,52 +125,66 @@ void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_s } self->state = RC_LBOARD_STATE_WAITING; + self->has_memrefs = 0; } int rc_lboard_size(const char* memaddr) { - rc_lboard_t* self; - rc_parse_state_t parse; - rc_memref_t* first_memref; - rc_init_parse_state(&parse, 0, 0, 0); - rc_init_parse_state_memrefs(&parse, &first_memref); + rc_lboard_with_memrefs_t* lboard; + rc_preparse_state_t preparse; + rc_init_preparse_state(&preparse); - self = RC_ALLOC(rc_lboard_t, &parse); - rc_parse_lboard_internal(self, memaddr, &parse); + lboard = RC_ALLOC(rc_lboard_with_memrefs_t, &preparse.parse); + rc_parse_lboard_internal(&lboard->lboard, memaddr, &preparse.parse); + rc_preparse_alloc_memrefs(NULL, &preparse); - rc_destroy_parse_state(&parse); - return parse.offset; + rc_destroy_preparse_state(&preparse); + return preparse.parse.offset; } -rc_lboard_t* rc_parse_lboard(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx) { - rc_lboard_t* self; - rc_parse_state_t parse; +rc_lboard_t* rc_parse_lboard(void* buffer, const char* memaddr, void* unused_L, int unused_funcs_idx) { + rc_lboard_with_memrefs_t* lboard; + rc_preparse_state_t preparse; + + (void)unused_L; + (void)unused_funcs_idx; if (!buffer || !memaddr) return 0; - rc_init_parse_state(&parse, buffer, L, funcs_ndx); + rc_init_preparse_state(&preparse); + lboard = RC_ALLOC(rc_lboard_with_memrefs_t, &preparse.parse); + rc_parse_lboard_internal(&lboard->lboard, memaddr, &preparse.parse); - self = RC_ALLOC(rc_lboard_t, &parse); - rc_init_parse_state_memrefs(&parse, &self->memrefs); + rc_reset_parse_state(&preparse.parse, buffer); + lboard = RC_ALLOC(rc_lboard_with_memrefs_t, &preparse.parse); + rc_preparse_alloc_memrefs(&lboard->memrefs, &preparse); - rc_parse_lboard_internal(self, memaddr, &parse); + rc_parse_lboard_internal(&lboard->lboard, memaddr, &preparse.parse); + lboard->lboard.has_memrefs = 1; - rc_destroy_parse_state(&parse); - return (parse.offset >= 0) ? self : 0; + rc_destroy_preparse_state(&preparse); + return (preparse.parse.offset >= 0) ? &lboard->lboard : NULL; } -int rc_evaluate_lboard(rc_lboard_t* self, int32_t* value, rc_peek_t peek, void* peek_ud, lua_State* L) { +static void rc_update_lboard_memrefs(rc_lboard_t* self, rc_peek_t peek, void* ud) { + if (self->has_memrefs) { + rc_lboard_with_memrefs_t* lboard = (rc_lboard_with_memrefs_t*)self; + rc_update_memref_values(&lboard->memrefs, peek, ud); + } +} + +int rc_evaluate_lboard(rc_lboard_t* self, int32_t* value, rc_peek_t peek, void* peek_ud, void* unused_L) { int start_ok, cancel_ok, submit_ok; - rc_update_memref_values(self->memrefs, peek, peek_ud); + rc_update_lboard_memrefs(self, peek, peek_ud); if (self->state == RC_LBOARD_STATE_INACTIVE || self->state == RC_LBOARD_STATE_DISABLED) return RC_LBOARD_STATE_INACTIVE; /* these are always tested once every frame, to ensure hit counts work properly */ - start_ok = rc_test_trigger(&self->start, peek, peek_ud, L); - cancel_ok = rc_test_trigger(&self->cancel, peek, peek_ud, L); - submit_ok = rc_test_trigger(&self->submit, peek, peek_ud, L); + start_ok = rc_test_trigger(&self->start, peek, peek_ud, unused_L); + cancel_ok = rc_test_trigger(&self->cancel, peek, peek_ud, unused_L); + submit_ok = rc_test_trigger(&self->submit, peek, peek_ud, unused_L); switch (self->state) { @@ -232,13 +241,13 @@ int rc_evaluate_lboard(rc_lboard_t* self, int32_t* value, rc_peek_t peek, void* switch (self->state) { case RC_LBOARD_STATE_STARTED: if (self->progress) { - *value = rc_evaluate_value(self->progress, peek, peek_ud, L); + *value = rc_evaluate_value(self->progress, peek, peek_ud, unused_L); break; } /* fallthrough */ /* to RC_LBOARD_STATE_TRIGGERED */ case RC_LBOARD_STATE_TRIGGERED: - *value = rc_evaluate_value(&self->value, peek, peek_ud, L); + *value = rc_evaluate_value(&self->value, peek, peek_ud, unused_L); break; default: diff --git a/deps/rcheevos/src/rcheevos/memref.c b/deps/rcheevos/src/rcheevos/memref.c index 0cbec67b85..5b5d80cf8b 100644 --- a/deps/rcheevos/src/rcheevos/memref.c +++ b/deps/rcheevos/src/rcheevos/memref.c @@ -6,41 +6,232 @@ #define MEMREF_PLACEHOLDER_ADDRESS 0xFFFFFFFF -rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, uint32_t address, uint8_t size, uint8_t is_indirect) { - rc_memref_t** next_memref; - rc_memref_t* memref; +rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, uint32_t address, uint8_t size) { + rc_memref_list_t* memref_list = NULL; + rc_memref_t* memref = NULL; + int i; - if (!is_indirect) { - /* attempt to find an existing memref that can be shared */ - next_memref = parse->first_memref; - while (*next_memref) { - memref = *next_memref; - if (!memref->value.is_indirect && memref->address == address && memref->value.size == size) - return memref; + for (i = 0; i < 2; i++) { + if (i == 0) { + if (!parse->existing_memrefs) + continue; - next_memref = &memref->next; + memref_list = &parse->existing_memrefs->memrefs; + } + else { + memref_list = &parse->memrefs->memrefs; } - /* no match found, create a new entry */ - memref = RC_ALLOC_SCRATCH(rc_memref_t, parse); - *next_memref = memref; + do + { + const rc_memref_t* memref_stop; + + memref = memref_list->items; + memref_stop = memref + memref_list->count; + + for (; memref < memref_stop; ++memref) { + if (memref->address == address && memref->value.size == size) + return memref; + } + + if (!memref_list->next) + break; + + memref_list = memref_list->next; + } while (1); } - else { - /* indirect references always create a new entry because we can't guarantee that the - * indirection amount will be the same between references. because they aren't shared, - * don't bother putting them in the chain. - */ - memref = RC_ALLOC(rc_memref_t, parse); + + /* no match found, find a place to put the new entry */ + memref_list = &parse->memrefs->memrefs; + while (memref_list->count == memref_list->capacity && memref_list->next) + memref_list = memref_list->next; + + /* create a new entry */ + if (memref_list->count < memref_list->capacity) { + memref = &memref_list->items[memref_list->count++]; + } else { + const int32_t old_offset = parse->offset; + + if (memref_list->capacity != 0) { + memref_list = memref_list->next = RC_ALLOC_SCRATCH(rc_memref_list_t, parse); + memref_list->next = NULL; + } + + memref_list->items = RC_ALLOC_ARRAY_SCRATCH(rc_memref_t, 8, parse); + memref_list->count = 1; + memref_list->capacity = 8; + memref_list->allocated = 0; + + memref = memref_list->items; + + /* in preparse mode, don't count this memory, we'll do a single allocation once we have + * the final total */ + if (!parse->buffer) + parse->offset = old_offset; } memset(memref, 0, sizeof(*memref)); - memref->address = address; + memref->value.memref_type = RC_MEMREF_TYPE_MEMREF; + memref->value.type = RC_VALUE_TYPE_UNSIGNED; memref->value.size = size; - memref->value.is_indirect = is_indirect; + memref->address = address; return memref; } +rc_modified_memref_t* rc_alloc_modified_memref(rc_parse_state_t* parse, uint8_t size, const rc_operand_t* parent, + uint8_t modifier_type, const rc_operand_t* modifier) { + rc_modified_memref_list_t* modified_memref_list = NULL; + rc_modified_memref_t* modified_memref = NULL; + int i = 0; + + for (i = 0; i < 2; i++) { + if (i == 0) { + if (!parse->existing_memrefs) + continue; + + modified_memref_list = &parse->existing_memrefs->modified_memrefs; + } + else { + modified_memref_list = &parse->memrefs->modified_memrefs; + } + + do { + const rc_modified_memref_t* memref_stop; + + modified_memref = modified_memref_list->items; + memref_stop = modified_memref + modified_memref_list->count; + + for (; modified_memref < memref_stop; ++modified_memref) { + if (modified_memref->memref.value.size == size && + modified_memref->modifier_type == modifier_type && + rc_operands_are_equal(&modified_memref->parent, parent) && + rc_operands_are_equal(&modified_memref->modifier, modifier)) { + return modified_memref; + } + } + + if (!modified_memref_list->next) + break; + + modified_memref_list = modified_memref_list->next; + } while (1); + } + + /* no match found, find a place to put the new entry */ + modified_memref_list = &parse->memrefs->modified_memrefs; + while (modified_memref_list->count == modified_memref_list->capacity && modified_memref_list->next) + modified_memref_list = modified_memref_list->next; + + /* create a new entry */ + if (modified_memref_list->count < modified_memref_list->capacity) { + modified_memref = &modified_memref_list->items[modified_memref_list->count++]; + } else { + const int32_t old_offset = parse->offset; + + if (modified_memref_list->capacity != 0) { + modified_memref_list = modified_memref_list->next = RC_ALLOC_SCRATCH(rc_modified_memref_list_t, parse); + modified_memref_list->next = NULL; + } + + modified_memref_list->items = RC_ALLOC_ARRAY_SCRATCH(rc_modified_memref_t, 8, parse); + modified_memref_list->count = 1; + modified_memref_list->capacity = 8; + modified_memref_list->allocated = 0; + + modified_memref = modified_memref_list->items; + + /* in preparse mode, don't count this memory, we'll do a single allocation once we have + * the final total */ + if (!parse->buffer) + parse->offset = old_offset; + } + + memset(modified_memref, 0, sizeof(*modified_memref)); + modified_memref->memref.value.memref_type = RC_MEMREF_TYPE_MODIFIED_MEMREF; + modified_memref->memref.value.size = size; + modified_memref->memref.value.type = rc_memsize_is_float(size) ? RC_VALUE_TYPE_FLOAT : RC_VALUE_TYPE_UNSIGNED; + memcpy(&modified_memref->parent, parent, sizeof(modified_memref->parent)); + memcpy(&modified_memref->modifier, modifier, sizeof(modified_memref->modifier)); + modified_memref->modifier_type = modifier_type; + modified_memref->memref.address = rc_operand_is_memref(modifier) ? modifier->value.memref->address : modifier->value.num; + + return modified_memref; +} + +void rc_memrefs_init(rc_memrefs_t* memrefs) +{ + memset(memrefs, 0, sizeof(*memrefs)); + + memrefs->memrefs.capacity = 32; + memrefs->memrefs.items = + (rc_memref_t*)malloc(memrefs->memrefs.capacity * sizeof(rc_memref_t)); + memrefs->memrefs.allocated = 1; + + memrefs->modified_memrefs.capacity = 16; + memrefs->modified_memrefs.items = + (rc_modified_memref_t*)malloc(memrefs->modified_memrefs.capacity * sizeof(rc_modified_memref_t)); + memrefs->modified_memrefs.allocated = 1; +} + +void rc_memrefs_destroy(rc_memrefs_t* memrefs) +{ + rc_memref_list_t* memref_list = &memrefs->memrefs; + rc_modified_memref_list_t* modified_memref_list = &memrefs->modified_memrefs; + + do { + rc_memref_list_t* current_memref_list = memref_list; + memref_list = memref_list->next; + + if (current_memref_list->allocated) { + if (current_memref_list->items) + free(current_memref_list->items); + + if (current_memref_list != &memrefs->memrefs) + free(current_memref_list); + } + } while (memref_list); + + do { + rc_modified_memref_list_t* current_modified_memref_list = modified_memref_list; + modified_memref_list = modified_memref_list->next; + + if (current_modified_memref_list->allocated) { + if (current_modified_memref_list->items) + free(current_modified_memref_list->items); + + if (current_modified_memref_list != &memrefs->modified_memrefs) + free(current_modified_memref_list); + } + } while (modified_memref_list); + + free(memrefs); +} + +uint32_t rc_memrefs_count_memrefs(const rc_memrefs_t* memrefs) +{ + uint32_t count = 0; + const rc_memref_list_t* memref_list = &memrefs->memrefs; + while (memref_list) { + count += memref_list->count; + memref_list = memref_list->next; + } + + return count; +} + +uint32_t rc_memrefs_count_modified_memrefs(const rc_memrefs_t* memrefs) +{ + uint32_t count = 0; + const rc_modified_memref_list_t* modified_memref_list = &memrefs->modified_memrefs; + while (modified_memref_list) { + count += modified_memref_list->count; + modified_memref_list = modified_memref_list->next; + } + + return count; +} + int rc_parse_memref(const char** memaddr, uint8_t* size, uint32_t* address) { const char* aux = *memaddr; char* end; @@ -77,7 +268,12 @@ int rc_parse_memref(const char** memaddr, uint8_t* size, uint32_t* address) { /* case 'y': case 'Y': 64 bit? */ /* case 'z': case 'Z': 128 bit? */ - case '0': case '1': case '2': case '3': case '4': + case '0': + if (*aux == 'x') /* user mistyped an extra 0x: 0x0xabcd */ + return RC_INVALID_MEMORY_OPERAND; + /* fallthrough */ + + case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': @@ -485,19 +681,12 @@ void rc_update_memref_value(rc_memref_value_t* memref, uint32_t new_value) { } } -void rc_update_memref_values(rc_memref_t* memref, rc_peek_t peek, void* ud) { - while (memref) { - /* indirect memory references are not shared and will be updated in rc_get_memref_value */ - if (!memref->value.is_indirect) - rc_update_memref_value(&memref->value, rc_peek_value(memref->address, memref->value.size, peek, ud)); +void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memrefs_t* memrefs) +{ + if (memrefs) + memset(memrefs, 0, sizeof(*memrefs)); - memref = memref->next; - } -} - -void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memref_t** memrefs) { - parse->first_memref = memrefs; - *memrefs = 0; + parse->memrefs = memrefs; } static uint32_t rc_get_memref_value_value(const rc_memref_value_t* memref, int operand_type) { @@ -520,12 +709,68 @@ static uint32_t rc_get_memref_value_value(const rc_memref_value_t* memref, int o } } -uint32_t rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_state_t* eval_state) { - /* if this is an indirect reference, handle the indirection. */ - if (memref->value.is_indirect) { - const uint32_t new_address = memref->address + eval_state->add_address; - rc_update_memref_value(&memref->value, rc_peek_value(new_address, memref->value.size, eval_state->peek, eval_state->peek_userdata)); +void rc_get_memref_value(rc_typed_value_t* value, rc_memref_t* memref, int operand_type) { + value->type = memref->value.type; + value->value.u32 = rc_get_memref_value_value(&memref->value, operand_type); +} + +uint32_t rc_get_modified_memref_value(const rc_modified_memref_t* memref, rc_peek_t peek, void* ud) { + rc_typed_value_t value, modifier; + + rc_evaluate_operand(&value, &memref->parent, NULL); + rc_evaluate_operand(&modifier, &memref->modifier, NULL); + + switch (memref->modifier_type) { + case RC_OPERATOR_INDIRECT_READ: + rc_typed_value_add(&value, &modifier); + rc_typed_value_convert(&value, RC_VALUE_TYPE_UNSIGNED); + value.value.u32 = rc_peek_value(value.value.u32, memref->memref.value.size, peek, ud); + value.type = memref->memref.value.type; + break; + + case RC_OPERATOR_SUB_PARENT: + rc_typed_value_negate(&value); + rc_typed_value_add(&value, &modifier); + rc_typed_value_convert(&value, memref->memref.value.type); + break; + + default: + rc_typed_value_combine(&value, &modifier, memref->modifier_type); + rc_typed_value_convert(&value, memref->memref.value.type); + break; } - return rc_get_memref_value_value(&memref->value, operand_type); + return value.value.u32; +} + +void rc_update_memref_values(rc_memrefs_t* memrefs, rc_peek_t peek, void* ud) { + rc_memref_list_t* memref_list; + rc_modified_memref_list_t* modified_memref_list; + + memref_list = &memrefs->memrefs; + do + { + rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_stop = memref + memref_list->count; + + for (; memref < memref_stop; ++memref) { + if (memref->value.type != RC_VALUE_TYPE_NONE) + rc_update_memref_value(&memref->value, rc_peek_value(memref->address, memref->value.size, peek, ud)); + } + + memref_list = memref_list->next; + } while (memref_list); + + modified_memref_list = &memrefs->modified_memrefs; + if (modified_memref_list->count) { + do { + rc_modified_memref_t* modified_memref = modified_memref_list->items; + const rc_modified_memref_t* modified_memref_stop = modified_memref + modified_memref_list->count; + + for (; modified_memref < modified_memref_stop; ++modified_memref) + rc_update_memref_value(&modified_memref->memref.value, rc_get_modified_memref_value(modified_memref, peek, ud)); + + modified_memref_list = modified_memref_list->next; + } while (modified_memref_list); + } } diff --git a/deps/rcheevos/src/rcheevos/operand.c b/deps/rcheevos/src/rcheevos/operand.c index 09ac65ee19..787c9c5851 100644 --- a/deps/rcheevos/src/rcheevos/operand.c +++ b/deps/rcheevos/src/rcheevos/operand.c @@ -5,67 +5,29 @@ #include #include -#ifndef RC_DISABLE_LUA - -RC_BEGIN_C_DECLS - -#include -#include - -RC_END_C_DECLS - -#endif /* RC_DISABLE_LUA */ - -static int rc_parse_operand_lua(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse) { +static int rc_parse_operand_func_call(rc_operand_t* self, const char** memaddr) { const char* aux = *memaddr; -#ifndef RC_DISABLE_LUA - const char* id; -#endif if (*aux++ != '@') { - return RC_INVALID_LUA_OPERAND; + return RC_INVALID_FUNC_OPERAND; } if (!isalpha((unsigned char)*aux)) { - return RC_INVALID_LUA_OPERAND; + return RC_INVALID_FUNC_OPERAND; } -#ifndef RC_DISABLE_LUA - id = aux; -#endif - while (isalnum((unsigned char)*aux) || *aux == '_') { aux++; } -#ifndef RC_DISABLE_LUA - - if (parse->L != 0) { - if (!lua_istable(parse->L, parse->funcs_ndx)) { - return RC_INVALID_LUA_OPERAND; - } - - lua_pushlstring(parse->L, id, aux - id); - lua_gettable(parse->L, parse->funcs_ndx); - - if (!lua_isfunction(parse->L, -1)) { - lua_pop(parse->L, 1); - return RC_INVALID_LUA_OPERAND; - } - - self->value.luafunc = luaL_ref(parse->L, LUA_REGISTRYINDEX); - } - -#else - (void)parse; -#endif /* RC_DISABLE_LUA */ - - self->type = RC_OPERAND_LUA; + self->type = RC_OPERAND_FUNC; + self->size = RC_MEMSIZE_32_BITS; + self->memref_access_type = RC_OPERAND_ADDRESS; *memaddr = aux; return RC_OK; } -static int rc_parse_operand_variable(rc_operand_t* self, const char** memaddr) { +static int rc_parse_operand_variable(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse) { const char* aux = *memaddr; size_t i; char varName[RC_VALUE_MAX_NAME_LENGTH + 1] = { 0 }; @@ -86,6 +48,16 @@ static int rc_parse_operand_variable(rc_operand_t* self, const char** memaddr) { ++aux; if (strcmp(varName, "recall") == 0) { + if (parse->remember.type == RC_OPERAND_NONE) { + self->value.memref = NULL; + self->size = RC_MEMSIZE_32_BITS; + self->memref_access_type = RC_OPERAND_ADDRESS; + } + else { + memcpy(self, &parse->remember, sizeof(*self)); + self->is_combining = 0; + self->memref_access_type = self->type; + } self->type = RC_OPERAND_RECALL; } else { /* process named variable when feature is available.*/ @@ -96,7 +68,7 @@ static int rc_parse_operand_variable(rc_operand_t* self, const char** memaddr) { return RC_OK; } -static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse, uint8_t is_indirect) { +static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse) { const char* aux = *memaddr; uint32_t address; uint8_t size; @@ -128,6 +100,8 @@ static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_ break; } + self->memref_access_type = self->type; + ret = rc_parse_memref(&aux, &self->size, &address); if (ret != RC_OK) return ret; @@ -137,13 +111,23 @@ static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_ /* if the shared size differs from the requested size and it's a prior operation, we * have to check to make sure both sizes use the same mask, or the prior value may be * updated when bits outside the mask are modified, which would make it look like the - * current value once the mask is applied. if the mask differs, create a new + * current value once the mask is applied. if the mask differs, create a new * non-shared record for tracking the prior data. */ if (rc_memref_mask(size) != rc_memref_mask(self->size)) size = self->size; } - self->value.memref = rc_alloc_memref(parse, address, size, is_indirect); + if (parse->indirect_parent.type != RC_OPERAND_NONE) { + rc_operand_t offset; + rc_operand_set_const(&offset, address); + + self->value.memref = (rc_memref_t*)rc_alloc_modified_memref(parse, + size, &parse->indirect_parent, RC_OPERATOR_INDIRECT_READ, &offset); + } + else { + self->value.memref = rc_alloc_memref(parse, address, size); + } + if (parse->offset < 0) return parse->offset; @@ -151,7 +135,7 @@ static int rc_parse_operand_memory(rc_operand_t* self, const char** memaddr, rc_ return RC_OK; } -int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indirect, rc_parse_state_t* parse) { +int rc_parse_operand(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse) { const char* aux = *memaddr; char* end; int ret; @@ -159,7 +143,7 @@ int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indire int negative; int allow_decimal = 0; - self->size = RC_MEMSIZE_32_BITS; + self->is_combining = 0; switch (*aux) { case 'h': case 'H': /* hex constant */ @@ -175,15 +159,14 @@ int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indire if (value > 0xffffffffU) value = 0xffffffffU; - self->type = RC_OPERAND_CONST; - self->value.num = (unsigned)value; + rc_operand_set_const(self, (unsigned)value); aux = end; break; case 'f': case 'F': /* floating point constant */ if (isalpha((unsigned char)aux[1])) { - ret = rc_parse_operand_memory(self, &aux, parse, is_indirect); + ret = rc_parse_operand_memory(self, &aux, parse); if (ret < 0) return ret; @@ -211,6 +194,7 @@ int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indire /* custom parser for decimal values to ignore locale */ unsigned long shift = 1; unsigned long fraction = 0; + double dbl_val; aux = end + 1; if (*aux < '0' || *aux > '9') @@ -231,19 +215,19 @@ int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indire /* non-zero fractional part, convert to double and merge in integer portion */ const double dbl_fraction = ((double)fraction) / ((double)shift); if (negative) - self->value.dbl = ((double)(-((long)value))) - dbl_fraction; + dbl_val = ((double)(-((long)value))) - dbl_fraction; else - self->value.dbl = (double)value + dbl_fraction; + dbl_val = (double)value + dbl_fraction; } else { /* fractional part is 0, just convert the integer portion */ if (negative) - self->value.dbl = (double)(-((long)value)); + dbl_val = (double)(-((long)value)); else - self->value.dbl = (double)value; + dbl_val = (double)value; } - self->type = RC_OPERAND_FP; + rc_operand_set_float_const(self, dbl_val); } else { /* not a floating point value, make sure something was read and advance the read pointer */ @@ -255,17 +239,15 @@ int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indire if (value > 0x7fffffffU) value = 0x7fffffffU; - self->type = RC_OPERAND_CONST; - if (negative) - self->value.num = (unsigned)(-((long)value)); + rc_operand_set_const(self, (unsigned)(-((long)value))); else - self->value.num = (unsigned)value; + rc_operand_set_const(self, (unsigned)value); } break; case '{': /* variable */ ++aux; - ret = rc_parse_operand_variable(self, &aux); + ret = rc_parse_operand_variable(self, &aux, parse); if (ret < 0) return ret; @@ -275,7 +257,7 @@ int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indire if (aux[1] == 'x' || aux[1] == 'X') { /* hex integer constant */ /* fallthrough */ /* to default */ default: - ret = rc_parse_operand_memory(self, &aux, parse, is_indirect); + ret = rc_parse_operand_memory(self, &aux, parse); if (ret < 0) return ret; @@ -292,14 +274,13 @@ int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indire if (value > 0xffffffffU) value = 0xffffffffU; - self->type = RC_OPERAND_CONST; - self->value.num = (unsigned)value; + rc_operand_set_const(self, (unsigned)value); aux = end; break; case '@': - ret = rc_parse_operand_lua(self, &aux, parse); + ret = rc_parse_operand_func_call(self, &aux); if (ret < 0) return ret; @@ -311,29 +292,77 @@ int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indire return RC_OK; } -#ifndef RC_DISABLE_LUA - -typedef struct { - rc_peek_t peek; - void* ud; -} -rc_luapeek_t; - -static int rc_luapeek(lua_State* L) { - uint32_t address = (uint32_t)luaL_checkinteger(L, 1); - uint32_t num_bytes = (uint32_t)luaL_checkinteger(L, 2); - rc_luapeek_t* luapeek = (rc_luapeek_t*)lua_touserdata(L, 3); - - uint32_t value = luapeek->peek(address, num_bytes, luapeek->ud); - - lua_pushinteger(L, value); - return 1; +void rc_operand_set_const(rc_operand_t* self, uint32_t value) { + self->size = RC_MEMSIZE_32_BITS; + self->type = RC_OPERAND_CONST; + self->memref_access_type = RC_OPERAND_NONE; + self->value.num = value; } -#endif /* RC_DISABLE_LUA */ +void rc_operand_set_float_const(rc_operand_t* self, double value) { + self->size = RC_MEMSIZE_FLOAT; + self->type = RC_OPERAND_FP; + self->memref_access_type = RC_OPERAND_NONE; + self->value.dbl = value; +} -int rc_operand_is_float_memref(const rc_operand_t* self) { - switch (self->size) { +int rc_operands_are_equal(const rc_operand_t* left, const rc_operand_t* right) { + if (left == right) + return 1; + + if (left->type != right->type) + return 0; + + switch (left->type) { + case RC_OPERAND_CONST: + return (left->value.num == right->value.num); + case RC_OPERAND_FP: + return (left->value.dbl == right->value.dbl); + case RC_OPERAND_RECALL: + return (left->value.memref == right->value.memref); + default: + break; + } + + /* comparing two memrefs - look for exact matches on type and size */ + if (left->size != right->size || left->value.memref->value.memref_type != right->value.memref->value.memref_type) + return 0; + + switch (left->value.memref->value.memref_type) { + case RC_MEMREF_TYPE_MODIFIED_MEMREF: + { + const rc_modified_memref_t* left_memref = (const rc_modified_memref_t*)left->value.memref; + const rc_modified_memref_t* right_memref = (const rc_modified_memref_t*)right->value.memref; + return (left_memref->modifier_type == right_memref->modifier_type && + rc_operands_are_equal(&left_memref->parent, &right_memref->parent) && + rc_operands_are_equal(&left_memref->modifier, &right_memref->modifier)); + } + + default: + return (left->value.memref->address == right->value.memref->address && + left->value.memref->value.size == right->value.memref->value.size); + } +} + +int rc_operator_is_modifying(int oper) { + switch (oper) { + case RC_OPERATOR_AND: + case RC_OPERATOR_XOR: + case RC_OPERATOR_DIV: + case RC_OPERATOR_MULT: + case RC_OPERATOR_MOD: + case RC_OPERATOR_ADD: + case RC_OPERATOR_SUB: + case RC_OPERATOR_NONE: /* NONE operator implies "* 1" */ + return 1; + + default: + return 0; + } +} + +int rc_memsize_is_float(uint8_t size) { + switch (size) { case RC_MEMSIZE_FLOAT: case RC_MEMSIZE_FLOAT_BE: case RC_MEMSIZE_DOUBLE32: @@ -347,11 +376,24 @@ int rc_operand_is_float_memref(const rc_operand_t* self) { } } -int rc_operand_is_memref(const rc_operand_t* self) { - switch (self->type) { +int rc_operand_is_float_memref(const rc_operand_t* self) { + if (!rc_operand_is_memref(self)) + return 0; + + if (self->value.memref->value.memref_type == RC_MEMREF_TYPE_MODIFIED_MEMREF) { + const rc_modified_memref_t* memref = (const rc_modified_memref_t*)self->value.memref; + if (memref->modifier_type != RC_OPERATOR_INDIRECT_READ) + return rc_memsize_is_float(self->value.memref->value.size); + } + + return rc_memsize_is_float(self->size); +} + +int rc_operand_type_is_memref(uint8_t type) { + switch (type) { case RC_OPERAND_CONST: case RC_OPERAND_FP: - case RC_OPERAND_LUA: + case RC_OPERAND_FUNC: case RC_OPERAND_RECALL: return 0; @@ -360,6 +402,21 @@ int rc_operand_is_memref(const rc_operand_t* self) { } } +int rc_operand_type_is_transform(uint8_t type) { + switch (type) { + case RC_OPERAND_BCD: + case RC_OPERAND_INVERTED: + return 1; + + default: + return 0; + } +} + +int rc_operand_is_memref(const rc_operand_t* self) { + return rc_operand_type_is_memref(self->type); +} + int rc_operand_is_recall(const rc_operand_t* self) { switch (self->type) { case RC_OPERAND_RECALL: @@ -374,10 +431,13 @@ int rc_operand_is_float(const rc_operand_t* self) { if (self->type == RC_OPERAND_FP) return 1; + if (self->type == RC_OPERAND_RECALL) + return rc_memsize_is_float(self->size); + return rc_operand_is_float_memref(self); } -uint32_t rc_transform_operand_value(uint32_t value, const rc_operand_t* self) { +static uint32_t rc_transform_operand_value(uint32_t value, const rc_operand_t* self) { switch (self->type) { case RC_OPERAND_BCD: @@ -465,11 +525,35 @@ uint32_t rc_transform_operand_value(uint32_t value, const rc_operand_t* self) { return value; } -void rc_evaluate_operand(rc_typed_value_t* result, rc_operand_t* self, rc_eval_state_t* eval_state) { -#ifndef RC_DISABLE_LUA - rc_luapeek_t luapeek; -#endif /* RC_DISABLE_LUA */ +void rc_operand_addsource(rc_operand_t* self, rc_parse_state_t* parse, uint8_t new_size) { + rc_modified_memref_t* modified_memref; + if ((self->type == RC_OPERAND_DELTA || self->type == RC_OPERAND_PRIOR) && + self->type == parse->addsource_parent.type) { + /* if adding prev(x) and prev(y), just add x and y and take the prev of that. + * same for adding prior(x) and prior(y). */ + rc_operand_t modifier; + memcpy(&modifier, self, sizeof(modifier)); + modifier.type = parse->addsource_parent.type = RC_OPERAND_ADDRESS; + + modified_memref = rc_alloc_modified_memref(parse, + new_size, &parse->addsource_parent, parse->addsource_oper, &modifier); + } + else { + modified_memref = rc_alloc_modified_memref(parse, + new_size, &parse->addsource_parent, parse->addsource_oper, self); + + /* the modified memref will contain the combination of modified values, take the current value from that */ + self->type = self->memref_access_type = RC_OPERAND_ADDRESS; + } + + self->value.memref = (rc_memref_t*)modified_memref; + + /* result of an AddSource operation is always a 32-bit integer (even if parent or modifier is a float) */ + self->size = RC_MEMSIZE_32_BITS; +} + +void rc_evaluate_operand(rc_typed_value_t* result, const rc_operand_t* self, rc_eval_state_t* eval_state) { /* step 1: read memory */ switch (self->type) { case RC_OPERAND_CONST: @@ -482,44 +566,32 @@ void rc_evaluate_operand(rc_typed_value_t* result, rc_operand_t* self, rc_eval_s result->value.f32 = (float)self->value.dbl; return; - case RC_OPERAND_LUA: + case RC_OPERAND_FUNC: + /* this feature was never actualized */ result->type = RC_VALUE_TYPE_UNSIGNED; result->value.u32 = 0; - -#ifndef RC_DISABLE_LUA - if (eval_state->L != 0) { - lua_rawgeti(eval_state->L, LUA_REGISTRYINDEX, self->value.luafunc); - lua_pushcfunction(eval_state->L, rc_luapeek); - - luapeek.peek = eval_state->peek; - luapeek.ud = eval_state->peek_userdata; - - lua_pushlightuserdata(eval_state->L, &luapeek); - - if (lua_pcall(eval_state->L, 2, 1, 0) == LUA_OK) { - if (lua_isboolean(eval_state->L, -1)) { - result->value.u32 = (uint32_t)lua_toboolean(eval_state->L, -1); - } - else { - result->value.u32 = (uint32_t)lua_tonumber(eval_state->L, -1); - } - } - - lua_pop(eval_state->L, 1); - } - -#endif /* RC_DISABLE_LUA */ - - break; - - case RC_OPERAND_RECALL: - result->type = eval_state->recall_value.type; - result->value = eval_state->recall_value.value; return; + case RC_OPERAND_RECALL: + if (!rc_operand_type_is_memref(self->memref_access_type)) { + rc_operand_t recall; + memcpy(&recall, self, sizeof(recall)); + recall.type = self->memref_access_type; + rc_evaluate_operand(result, &recall, eval_state); + return; + } + + if (!self->value.memref) { + result->type = RC_VALUE_TYPE_UNSIGNED; + result->value.u32 = 0; + return; + } + + rc_get_memref_value(result, self->value.memref, self->memref_access_type); + break; + default: - result->type = RC_VALUE_TYPE_UNSIGNED; - result->value.u32 = rc_get_memref_value(self->value.memref, self->type, eval_state); + rc_get_memref_value(result, self->value.memref, self->type); break; } diff --git a/deps/rcheevos/src/rcheevos/rc_internal.h b/deps/rcheevos/src/rcheevos/rc_internal.h index fa913fec84..7c013d9dc1 100644 --- a/deps/rcheevos/src/rcheevos/rc_internal.h +++ b/deps/rcheevos/src/rcheevos/rc_internal.h @@ -13,27 +13,115 @@ typedef struct rc_scratch_string { } rc_scratch_string_t; -#define RC_ALLOW_ALIGN(T) struct __align_ ## T { char ch; T t; }; +typedef struct rc_modified_memref_t { + rc_memref_t memref; /* for compatibility with rc_operand_t.value.memref */ + rc_operand_t parent; /* The parent memref this memref is derived from (type will always be a memref type) */ + rc_operand_t modifier; /* The modifier to apply to the parent. */ + uint8_t modifier_type; /* How to apply the modifier to the parent. (RC_OPERATOR_*) */ +} +rc_modified_memref_t; + +typedef struct rc_memref_list_t { + rc_memref_t* items; + struct rc_memref_list_t* next; + uint16_t count; + uint16_t capacity; + uint8_t allocated; +} rc_memref_list_t; + +typedef struct rc_modified_memref_list_t { + rc_modified_memref_t* items; + struct rc_modified_memref_list_t* next; + uint16_t count; + uint16_t capacity; + uint8_t allocated; +} rc_modified_memref_list_t; + +typedef struct rc_memrefs_t { + rc_memref_list_t memrefs; + rc_modified_memref_list_t modified_memrefs; +} rc_memrefs_t; + +typedef struct rc_trigger_with_memrefs_t { + rc_trigger_t trigger; + rc_memrefs_t memrefs; +} rc_trigger_with_memrefs_t; + +typedef struct rc_lboard_with_memrefs_t { + rc_lboard_t lboard; + rc_memrefs_t memrefs; +} rc_lboard_with_memrefs_t; + +typedef struct rc_richpresence_with_memrefs_t { + rc_richpresence_t richpresence; + rc_memrefs_t memrefs; +} rc_richpresence_with_memrefs_t; + +typedef struct rc_value_with_memrefs_t { + rc_value_t value; + rc_memrefs_t memrefs; +} rc_value_with_memrefs_t; + +/* enum helpers for natvis expansion. Have to use a struct to define the mapping, + * and a single field to allow the conditional logic to switch on the value */ +typedef struct __rc_bool_enum_t { uint8_t value; } __rc_bool_enum_t; +typedef struct __rc_memsize_enum_t { uint8_t value; } __rc_memsize_enum_t; +typedef struct __rc_memsize_enum_func_t { uint8_t value; } __rc_memsize_enum_func_t; +typedef struct __rc_operand_enum_t { uint8_t value; } __rc_operand_enum_t; +typedef struct __rc_value_type_enum_t { uint8_t value; } __rc_value_type_enum_t; +typedef struct __rc_memref_type_enum_t { uint8_t value; } __rc_memref_type_enum_t; +typedef struct __rc_condition_enum_t { uint8_t value; } __rc_condition_enum_t; +typedef struct __rc_condition_enum_str_t { uint8_t value; } __rc_condition_enum_str_t; +typedef struct __rc_condset_list_t { rc_condset_t* first_condset; } __rc_condset_list_t; +typedef struct __rc_operator_enum_t { uint8_t value; } __rc_operator_enum_t; +typedef struct __rc_operator_enum_str_t { uint8_t value; } __rc_operator_enum_str_t; +typedef struct __rc_operand_memref_t { rc_operand_t operand; } __rc_operand_memref_t; /* requires &rc_operand_t to be the same as &rc_operand_t.value.memref */ +typedef struct __rc_value_list_t { rc_value_t* first_value; } __rc_value_list_t; +typedef struct __rc_trigger_state_enum_t { uint8_t value; } __rc_trigger_state_enum_t; +typedef struct __rc_lboard_state_enum_t { uint8_t value; } __rc_lboard_state_enum_t; +typedef struct __rc_richpresence_display_list_t { rc_richpresence_display_t* first_display; } __rc_richpresence_display_list_t; +typedef struct __rc_richpresence_display_part_list_t { rc_richpresence_display_part_t* display; } __rc_richpresence_display_part_list_t; +typedef struct __rc_richpresence_lookup_list_t { rc_richpresence_lookup_t* first_lookup; } __rc_richpresence_lookup_list_t; +typedef struct __rc_format_enum_t { uint8_t value; } __rc_format_enum_t; + +#define RC_ALLOW_ALIGN(T) struct __align_ ## T { uint8_t ch; T t; }; + RC_ALLOW_ALIGN(rc_condition_t) RC_ALLOW_ALIGN(rc_condset_t) +RC_ALLOW_ALIGN(rc_modified_memref_t) RC_ALLOW_ALIGN(rc_lboard_t) +RC_ALLOW_ALIGN(rc_lboard_with_memrefs_t) RC_ALLOW_ALIGN(rc_memref_t) +RC_ALLOW_ALIGN(rc_memref_list_t) +RC_ALLOW_ALIGN(rc_memrefs_t) +RC_ALLOW_ALIGN(rc_modified_memref_list_t) RC_ALLOW_ALIGN(rc_operand_t) RC_ALLOW_ALIGN(rc_richpresence_t) RC_ALLOW_ALIGN(rc_richpresence_display_t) RC_ALLOW_ALIGN(rc_richpresence_display_part_t) RC_ALLOW_ALIGN(rc_richpresence_lookup_t) RC_ALLOW_ALIGN(rc_richpresence_lookup_item_t) +RC_ALLOW_ALIGN(rc_richpresence_with_memrefs_t) RC_ALLOW_ALIGN(rc_scratch_string_t) RC_ALLOW_ALIGN(rc_trigger_t) +RC_ALLOW_ALIGN(rc_trigger_with_memrefs_t) RC_ALLOW_ALIGN(rc_value_t) +RC_ALLOW_ALIGN(rc_value_with_memrefs_t) RC_ALLOW_ALIGN(char) #define RC_ALIGNOF(T) (sizeof(struct __align_ ## T) - sizeof(T)) -#define RC_OFFSETOF(o, t) (int)((char*)&(o.t) - (char*)&(o)) +#define RC_OFFSETOF(o, t) (int)((uint8_t*)&(o.t) - (uint8_t*)&(o)) #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))) +#define RC_ALLOC_ARRAY(t, n, p) ((t*)rc_alloc((p)->buffer, &(p)->offset, (n) * sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) +#define RC_ALLOC_ARRAY_SCRATCH(t, n, p) ((t*)rc_alloc_scratch((p)->buffer, &(p)->offset, (n) * sizeof(t), RC_ALIGNOF(t), &(p)->scratch, RC_OFFSETOF((p)->scratch.objs, __ ## t))) + +#define RC_ALLOC_WITH_TRAILING(container_type, trailing_type, trailing_field, trailing_count, parse) ((container_type*)rc_alloc(\ + (parse)->buffer, &(parse)->offset, \ + RC_OFFSETOF((*(container_type*)NULL),trailing_field) + trailing_count * sizeof(trailing_type), \ + RC_ALIGNOF(container_type), &(parse)->scratch, 0)) +#define RC_GET_TRAILING(container_pointer, container_type, trailing_type, trailing_field) (trailing_type*)(&((container_type*)(container_pointer))->trailing_field) /* 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)) @@ -45,17 +133,49 @@ typedef struct { struct objs { rc_condition_t* __rc_condition_t; rc_condset_t* __rc_condset_t; + rc_modified_memref_t* __rc_modified_memref_t; rc_lboard_t* __rc_lboard_t; + rc_lboard_with_memrefs_t* __rc_lboard_with_memrefs_t; rc_memref_t* __rc_memref_t; + rc_memref_list_t* __rc_memref_list_t; + rc_memrefs_t* __rc_memrefs_t; + rc_modified_memref_list_t* __rc_modified_memref_list_t; rc_operand_t* __rc_operand_t; rc_richpresence_t* __rc_richpresence_t; rc_richpresence_display_t* __rc_richpresence_display_t; rc_richpresence_display_part_t* __rc_richpresence_display_part_t; rc_richpresence_lookup_t* __rc_richpresence_lookup_t; rc_richpresence_lookup_item_t* __rc_richpresence_lookup_item_t; + rc_richpresence_with_memrefs_t* __rc_richpresence_with_memrefs_t; rc_scratch_string_t __rc_scratch_string_t; rc_trigger_t* __rc_trigger_t; + rc_trigger_with_memrefs_t* __rc_trigger_with_memrefs_t; rc_value_t* __rc_value_t; + rc_value_with_memrefs_t* __rc_value_with_memrefs_t; + + /* these fields aren't actually used by the code, but they force the + * virtual enum wrapper types to exist so natvis can use them */ + union { + __rc_bool_enum_t boolean; + __rc_memsize_enum_t memsize; + __rc_memsize_enum_func_t memsize_func; + __rc_operand_enum_t operand; + __rc_value_type_enum_t value_type; + __rc_memref_type_enum_t memref_type; + __rc_condition_enum_t condition; + __rc_condition_enum_str_t condition_str; + __rc_condset_list_t condset_list; + __rc_operator_enum_t oper; + __rc_operator_enum_str_t oper_str; + __rc_operand_memref_t operand_memref; + __rc_value_list_t value_list; + __rc_trigger_state_enum_t trigger_state; + __rc_lboard_state_enum_t lboard_state; + __rc_richpresence_display_list_t richpresence_display_list; + __rc_richpresence_display_part_list_t richpresence_display_part_list; + __rc_richpresence_lookup_list_t richpresence_lookup_list; + __rc_format_enum_t format; + } natvis_extension; } objs; } rc_scratch_t; @@ -78,72 +198,119 @@ typedef struct { } rc_typed_value_t; +enum { + RC_MEMREF_TYPE_MEMREF, /* rc_memref_t */ + RC_MEMREF_TYPE_MODIFIED_MEMREF, /* rc_modified_memref_t */ + RC_MEMREF_TYPE_VALUE /* rc_value_t */ +}; + #define RC_MEASURED_UNKNOWN 0xFFFFFFFF +#define RC_OPERAND_NONE 0xFF typedef struct { - rc_typed_value_t add_value;/* AddSource/SubSource */ - int32_t add_hits; /* AddHits */ - uint32_t add_address; /* AddAddress */ - + /* memory accessors */ rc_peek_t peek; void* peek_userdata; - lua_State* L; - rc_typed_value_t measured_value; /* Measured */ - rc_typed_value_t recall_value; /* Set by RC_CONDITION_REMEMBER */ - uint8_t was_reset; /* ResetIf triggered */ - uint8_t has_hits; /* one of more hit counts is non-zero */ - uint8_t primed; /* true if all non-Trigger conditions are true */ + /* processing state */ + rc_typed_value_t measured_value; /* captured Measured value */ + int32_t add_hits; /* AddHits/SubHits accumulator */ + uint8_t is_true; /* true if all conditions are true */ + uint8_t is_primed; /* true if all non-Trigger conditions are true */ + uint8_t is_paused; /* true if one or more PauseIf conditions is true */ + uint8_t can_measure; /* false if the measured value should be ignored */ uint8_t measured_from_hits; /* true if the measured_value came from a condition's hit count */ - uint8_t was_cond_reset; /* ResetNextIf triggered */ + uint8_t and_next; /* true if the previous condition was AndNext true */ + uint8_t or_next; /* true if the previous condition was OrNext true */ + uint8_t reset_next; /* true if the previous condition was ResetNextIf true */ + uint8_t stop_processing; /* true to abort the processing loop */ + + /* result state */ + uint8_t has_hits; /* true if one of more hit counts is non-zero */ + uint8_t was_reset; /* true if one or more ResetIf conditions is true */ + uint8_t was_cond_reset; /* true if one or more ResetNextIf conditions is true */ + + /* control settings */ + uint8_t can_short_curcuit; /* allows logic processing to stop as soon as a false condition is encountered */ } rc_eval_state_t; typedef struct { int32_t offset; - lua_State* L; - int funcs_ndx; - void* buffer; rc_scratch_t scratch; - rc_memref_t** first_memref; + rc_memrefs_t* memrefs; + rc_memrefs_t* existing_memrefs; rc_value_t** variables; uint32_t measured_target; int lines_read; + rc_operand_t addsource_parent; + rc_operand_t indirect_parent; + rc_operand_t remember; + uint8_t addsource_oper; + + uint8_t is_value; uint8_t has_required_hits; uint8_t measured_as_percent; + uint8_t ignore_non_parse_errors; } rc_parse_state_t; -void rc_init_parse_state(rc_parse_state_t* parse, void* buffer, lua_State* L, int funcs_ndx); -void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memref_t** memrefs); -void rc_init_parse_state_variables(rc_parse_state_t* parse, rc_value_t** variables); +typedef struct rc_preparse_state_t { + rc_parse_state_t parse; + rc_memrefs_t memrefs; +} rc_preparse_state_t; + +void rc_init_parse_state(rc_parse_state_t* parse, void* buffer); +void rc_init_parse_state_memrefs(rc_parse_state_t* parse, rc_memrefs_t* memrefs); +void rc_reset_parse_state(rc_parse_state_t* parse, void* buffer); void rc_destroy_parse_state(rc_parse_state_t* parse); +void rc_init_preparse_state(rc_preparse_state_t* preparse); +void rc_preparse_alloc_memrefs(rc_memrefs_t* memrefs, rc_preparse_state_t* preparse); +void rc_preparse_reserve_memrefs(rc_preparse_state_t* preparse, rc_memrefs_t* memrefs); +void rc_preparse_copy_memrefs(rc_parse_state_t* parse, rc_memrefs_t* memrefs); +void rc_destroy_preparse_state(rc_preparse_state_t *preparse); void* rc_alloc(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset); void* rc_alloc_scratch(void* pointer, int32_t* offset, uint32_t size, uint32_t alignment, rc_scratch_t* scratch, uint32_t scratch_object_pointer_offset); char* rc_alloc_str(rc_parse_state_t* parse, const char* text, size_t length); -rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, uint32_t address, uint8_t size, uint8_t is_indirect); +rc_memref_t* rc_alloc_memref(rc_parse_state_t* parse, uint32_t address, uint8_t size); +rc_modified_memref_t* rc_alloc_modified_memref(rc_parse_state_t* parse, uint8_t size, const rc_operand_t* parent, + uint8_t modifier_type, const rc_operand_t* modifier); int rc_parse_memref(const char** memaddr, uint8_t* size, uint32_t* address); -void rc_update_memref_values(rc_memref_t* memref, rc_peek_t peek, void* ud); +void rc_update_memref_values(rc_memrefs_t* memrefs, rc_peek_t peek, void* ud); void rc_update_memref_value(rc_memref_value_t* memref, uint32_t value); -uint32_t rc_get_memref_value(rc_memref_t* memref, int operand_type, rc_eval_state_t* eval_state); +void rc_get_memref_value(rc_typed_value_t* value, rc_memref_t* memref, int operand_type); +uint32_t rc_get_modified_memref_value(const rc_modified_memref_t* memref, rc_peek_t peek, void* ud); uint8_t rc_memref_shared_size(uint8_t size); uint32_t rc_memref_mask(uint8_t size); void rc_transform_memref_value(rc_typed_value_t* value, uint8_t size); uint32_t rc_peek_value(uint32_t address, uint8_t size, rc_peek_t peek, void* ud); +void rc_memrefs_init(rc_memrefs_t* memrefs); +void rc_memrefs_destroy(rc_memrefs_t* memrefs); +uint32_t rc_memrefs_count_memrefs(const rc_memrefs_t* memrefs); +uint32_t rc_memrefs_count_modified_memrefs(const rc_memrefs_t* memrefs); + void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_parse_state_t* parse); int rc_trigger_state_active(int state); +rc_memrefs_t* rc_trigger_get_memrefs(rc_trigger_t* self); -rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, int is_value); +typedef struct rc_condset_with_trailing_conditions_t { + rc_condset_t condset; + rc_condition_t conditions[2]; +} rc_condset_with_trailing_conditions_t; +RC_ALLOW_ALIGN(rc_condset_with_trailing_conditions_t) + +rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse); int rc_test_condset(rc_condset_t* self, rc_eval_state_t* eval_state); void rc_reset_condset(rc_condset_t* self); +rc_condition_t* rc_condset_get_conditions(rc_condset_t* self); enum { RC_PROCESSING_COMPARE_DEFAULT = 0, @@ -161,24 +328,38 @@ enum { RC_PROCESSING_COMPARE_ALWAYS_FALSE }; -rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse, uint8_t is_indirect); +rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse); +void rc_parse_condition_internal(rc_condition_t* self, const char** memaddr, rc_parse_state_t* parse); +void rc_condition_update_parse_state(rc_condition_t* condition, rc_parse_state_t* parse); 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); int rc_condition_is_combining(const rc_condition_t* self); +void rc_condition_convert_to_operand(const rc_condition_t* condition, rc_operand_t* operand, rc_parse_state_t* parse); +const rc_operand_t* rc_condition_get_real_operand1(const rc_condition_t* self); -int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t 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_parse_operand(rc_operand_t* self, const char** memaddr, rc_parse_state_t* parse); +void rc_evaluate_operand(rc_typed_value_t* value, const rc_operand_t* self, rc_eval_state_t* eval_state); +int rc_operator_is_modifying(int oper); +int rc_memsize_is_float(uint8_t size); int rc_operand_is_float_memref(const rc_operand_t* self); int rc_operand_is_float(const rc_operand_t* self); int rc_operand_is_recall(const rc_operand_t* self); +int rc_operand_type_is_memref(uint8_t type); +int rc_operand_type_is_transform(uint8_t type); +int rc_operands_are_equal(const rc_operand_t* left, const rc_operand_t* right); +void rc_operand_addsource(rc_operand_t* self, rc_parse_state_t* parse, uint8_t new_size); +void rc_operand_set_const(rc_operand_t* self, uint32_t value); +void rc_operand_set_float_const(rc_operand_t* self, double value); int rc_is_valid_variable_character(char ch, int is_first); 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); +int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t peek, void* ud); 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, size_t memaddr_len, rc_parse_state_t* parse); -void rc_update_variables(rc_value_t* variable, rc_peek_t peek, void* ud, lua_State* L); +rc_value_t* rc_alloc_variable(const char* memaddr, size_t memaddr_len, rc_parse_state_t* parse); +uint32_t rc_count_values(const rc_value_t* values); +void rc_update_values(rc_value_t* values, rc_peek_t peek, void* ud); +void rc_reset_values(rc_value_t* values); 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); @@ -187,6 +368,7 @@ void rc_typed_value_divide(rc_typed_value_t* value, const rc_typed_value_t* amou void rc_typed_value_modulus(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_combine(rc_typed_value_t* value, rc_typed_value_t* amount, uint8_t oper); void rc_typed_value_from_memref_value(rc_typed_value_t* value, const rc_memref_value_t* memref); int rc_format_typed_value(char* buffer, size_t size, const rc_typed_value_t* value, int format); @@ -195,6 +377,11 @@ void rc_parse_lboard_internal(rc_lboard_t* self, const char* memaddr, rc_parse_s int rc_lboard_state_active(int state); void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, rc_parse_state_t* parse); +rc_memrefs_t* rc_richpresence_get_memrefs(rc_richpresence_t* self); +void rc_reset_richpresence_triggers(rc_richpresence_t* self); + +int rc_validate_memrefs(const rc_memrefs_t* memrefs, char result[], const size_t result_size, uint32_t max_address); +int rc_validate_memrefs_for_console(const rc_memrefs_t* memrefs, char result[], const size_t result_size, uint32_t console_id); RC_END_C_DECLS diff --git a/deps/rcheevos/src/rcheevos/richpresence.c b/deps/rcheevos/src/rcheevos/richpresence.c index 81fc56a100..d372ecb2e6 100644 --- a/deps/rcheevos/src/rcheevos/richpresence.c +++ b/deps/rcheevos/src/rcheevos/richpresence.c @@ -13,29 +13,67 @@ enum { RC_FORMAT_UNICODECHAR = 105 }; -static rc_memref_value_t* rc_alloc_helper_variable_memref_value(const char* memaddr, int memaddr_len, rc_parse_state_t* parse) { - const char* end; - rc_value_t* variable; - uint32_t address; - uint8_t size; +static void rc_alloc_helper_variable_memref_value(rc_richpresence_display_part_t* part, const char* memaddr, int memaddr_len, rc_parse_state_t* parse) { + rc_preparse_state_t preparse; + const char* test_memaddr = memaddr; + rc_condset_t* condset; + rc_value_t* value; + int32_t size; - /* single memory reference lookups without a modifier flag can be handled without a variable */ - end = memaddr; - if (rc_parse_memref(&end, &size, &address) == RC_OK) { - /* make sure the entire memaddr was consumed. if not, there's an operator and it's a comparison, not a memory reference */ - if (end == &memaddr[memaddr_len]) { - /* if it's not a derived size, we can reference the memref directly */ - if (rc_memref_shared_size(size) == size) - return &rc_alloc_memref(parse, address, size, 0)->value; + part->value.type = RC_OPERAND_NONE; + + /* if the expression can be represented as just a memory reference, do so */ + rc_init_preparse_state(&preparse); + preparse.parse.existing_memrefs = parse->memrefs; + value = RC_ALLOC(rc_value_t, &preparse.parse); + rc_parse_value_internal(value, &test_memaddr, &preparse.parse); + + size = preparse.parse.offset; + if (size < 0) { + parse->offset = size; + rc_destroy_preparse_state(&preparse); + return; + } + + /* ensure new needed memrefs are allocated in the primary buffer */ + rc_preparse_copy_memrefs(parse, &preparse.memrefs); + + /* parse the value into the scratch buffer so we can look at it */ + rc_reset_parse_state(&preparse.parse, rc_buffer_alloc(&preparse.parse.scratch.buffer, (size_t)size)); + preparse.parse.memrefs = parse->memrefs; + preparse.parse.existing_memrefs = parse->existing_memrefs; + value = RC_ALLOC(rc_value_t, &preparse.parse); + test_memaddr = memaddr; + rc_parse_value_internal(value, &test_memaddr, &preparse.parse); + + condset = value->conditions; + if (condset && !condset->next) { + /* single value - if it's only "measured" and "indirect" conditions, we can simplify to a memref */ + if (condset->num_measured_conditions && + !condset->num_pause_conditions && !condset->num_reset_conditions && + !condset->num_other_conditions && !condset->num_hittarget_conditions) { + rc_condition_t* condition = condset->conditions; + for (; condition; condition = condition->next) { + if (condition->type == RC_CONDITION_MEASURED && condition->required_hits == 0) { + memcpy(&part->value, &condition->operand1, sizeof(condition->operand1)); + break; + } + } } } - /* not a simple memory reference, need to create a variable */ - variable = rc_alloc_helper_variable(memaddr, memaddr_len, parse); - if (!variable) - return NULL; + rc_destroy_preparse_state(&preparse); - return &variable->value; + /* could not express value with just a memory reference, create a helper variable */ + if (part->value.type == RC_OPERAND_NONE) { + value = rc_alloc_variable(memaddr, memaddr_len, parse); + if (value) { + part->value.value.memref = (rc_memref_t*)&value->value; + part->value.type = RC_OPERAND_ADDRESS; + part->value.size = RC_MEMSIZE_32_BITS; + part->value.memref_access_type = RC_OPERAND_ADDRESS; + } + } } static const char* rc_parse_line(const char* line, const char** end, rc_parse_state_t* parse) { @@ -192,7 +230,8 @@ static rc_richpresence_display_t* rc_parse_richpresence_display_internal(const c {"Fixed1", 6, RC_FORMAT_FIXED1}, {"Fixed2", 6, RC_FORMAT_FIXED2}, {"Fixed3", 6, RC_FORMAT_FIXED3}, - {"Unsigned", 8, RC_FORMAT_UNSIGNED_VALUE} + {"Unsigned", 8, RC_FORMAT_UNSIGNED_VALUE}, + {"Unformatted", 11, RC_FORMAT_UNFORMATTED} }; size_t i; @@ -220,7 +259,7 @@ static rc_richpresence_display_t* rc_parse_richpresence_display_internal(const c part->text = rc_alloc_str(parse, in, (int)(ptr - in)); } else if (part->display_type != RC_FORMAT_UNKNOWN_MACRO) { - part->value = rc_alloc_helper_variable_memref_value(line, (int)(ptr - line), parse); + rc_alloc_helper_variable_memref_value(part, line, (int)(ptr - line), parse); if (parse->offset < 0) return 0; @@ -468,6 +507,8 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, int display_line = 0; int chars; + self->values = NULL; + /* special case for empty script to return 1 line read */ if (!*script) { parse->lines_read = 1; @@ -496,6 +537,13 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, } else if (strncmp(line, "Format:", 7) == 0) { line += 7; + if (endline - line == 11 && memcmp(line, "Unformatted", 11) == 0) { + /* for backwards compatibility with the comma rollout, we allow old scripts + * to define an Unformatted type mapped to VALUE, and new versions will ignore + * the definition and use the built-in macro. skip the next line (FormatType=) */ + line = rc_parse_line(nextline, &endline, parse); + continue; + } lookup = RC_ALLOC_SCRATCH(rc_richpresence_lookup_t, parse); lookup->name = rc_alloc_str(parse, line, (int)(endline - line)); @@ -558,11 +606,20 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, *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 (line != ptr) { + /* incomplete read */ + parse->offset = RC_INVALID_OPERATOR; + return; + } + + (*nextdisplay)->has_required_hits = parse->has_required_hits; + if (parse->buffer) nextdisplay = &((*nextdisplay)->next); } @@ -593,6 +650,7 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, /* finalize */ *nextdisplay = 0; + self->has_memrefs = 0; if (!hasdisplay && parse->offset > 0) { parse->offset = RC_MISSING_DISPLAY_STRING; @@ -600,56 +658,78 @@ void rc_parse_richpresence_internal(rc_richpresence_t* self, const char* script, } int rc_richpresence_size_lines(const char* script, int* lines_read) { - rc_richpresence_t* self; - rc_parse_state_t parse; - rc_memref_t* first_memref; - rc_value_t* variables; - rc_init_parse_state(&parse, 0, 0, 0); - rc_init_parse_state_memrefs(&parse, &first_memref); - rc_init_parse_state_variables(&parse, &variables); + rc_richpresence_with_memrefs_t* richpresence; + rc_preparse_state_t preparse; + rc_init_preparse_state(&preparse); - self = RC_ALLOC(rc_richpresence_t, &parse); - rc_parse_richpresence_internal(self, script, &parse); + richpresence = RC_ALLOC(rc_richpresence_with_memrefs_t, &preparse.parse); + preparse.parse.variables = &richpresence->richpresence.values; + rc_parse_richpresence_internal(&richpresence->richpresence, script, &preparse.parse); + rc_preparse_alloc_memrefs(NULL, &preparse); if (lines_read) - *lines_read = parse.lines_read; + *lines_read = preparse.parse.lines_read; - rc_destroy_parse_state(&parse); - return parse.offset; + rc_destroy_preparse_state(&preparse); + return preparse.parse.offset; } int rc_richpresence_size(const char* script) { return rc_richpresence_size_lines(script, NULL); } -rc_richpresence_t* rc_parse_richpresence(void* buffer, const char* script, lua_State* L, int funcs_ndx) { - rc_richpresence_t* self; - rc_parse_state_t parse; +rc_richpresence_t* rc_parse_richpresence(void* buffer, const char* script, void* unused_L, int unused_funcs_idx) { + rc_richpresence_with_memrefs_t* richpresence; + rc_preparse_state_t preparse; + + (void)unused_L; + (void)unused_funcs_idx; if (!buffer || !script) return NULL; - rc_init_parse_state(&parse, buffer, L, funcs_ndx); + rc_init_preparse_state(&preparse); + richpresence = RC_ALLOC(rc_richpresence_with_memrefs_t, &preparse.parse); + preparse.parse.variables = &richpresence->richpresence.values; + rc_parse_richpresence_internal(&richpresence->richpresence, script, &preparse.parse); - self = RC_ALLOC(rc_richpresence_t, &parse); - rc_init_parse_state_memrefs(&parse, &self->memrefs); - rc_init_parse_state_variables(&parse, &self->variables); + rc_reset_parse_state(&preparse.parse, buffer); + richpresence = RC_ALLOC(rc_richpresence_with_memrefs_t, &preparse.parse); + preparse.parse.variables = &richpresence->richpresence.values; + rc_preparse_alloc_memrefs(&richpresence->memrefs, &preparse); - rc_parse_richpresence_internal(self, script, &parse); + rc_parse_richpresence_internal(&richpresence->richpresence, script, &preparse.parse); + richpresence->richpresence.has_memrefs = 1; - rc_destroy_parse_state(&parse); - return (parse.offset >= 0) ? self : NULL; + rc_destroy_preparse_state(&preparse); + return (preparse.parse.offset >= 0) ? &richpresence->richpresence : NULL; } -void rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud, lua_State* L) { +static void rc_update_richpresence_memrefs(rc_richpresence_t* self, rc_peek_t peek, void* ud) { + if (self->has_memrefs) { + rc_richpresence_with_memrefs_t* richpresence = (rc_richpresence_with_memrefs_t*)self; + rc_update_memref_values(&richpresence->memrefs, peek, ud); + } +} + +rc_memrefs_t* rc_richpresence_get_memrefs(rc_richpresence_t* self) { + if (self->has_memrefs) { + rc_richpresence_with_memrefs_t* richpresence = (rc_richpresence_with_memrefs_t*)self; + return &richpresence->memrefs; + } + + return NULL; +} + +void rc_update_richpresence(rc_richpresence_t* richpresence, rc_peek_t peek, void* peek_ud, void* unused_L) { rc_richpresence_display_t* display; - rc_update_memref_values(richpresence->memrefs, peek, peek_ud); - rc_update_variables(richpresence->variables, peek, peek_ud, L); + rc_update_richpresence_memrefs(richpresence, peek, peek_ud); + rc_update_values(richpresence->values, peek, peek_ud); for (display = richpresence->first_display; display; display = display->next) { - if (display->trigger.has_required_hits) - rc_test_trigger(&display->trigger, peek, peek_ud, L); + if (display->has_required_hits) + rc_test_trigger(&display->trigger, peek, peek_ud, unused_L); } } @@ -671,7 +751,7 @@ static int rc_evaluate_richpresence_display(rc_richpresence_display_part_t* part break; case RC_FORMAT_LOOKUP: - rc_typed_value_from_memref_value(&value, part->value); + rc_evaluate_operand(&value, &part->value, NULL); rc_typed_value_convert(&value, RC_VALUE_TYPE_UNSIGNED); text = part->lookup->default_label; @@ -698,7 +778,7 @@ static int rc_evaluate_richpresence_display(rc_richpresence_display_part_t* part value.type = RC_VALUE_TYPE_UNSIGNED; do { - value.value.u32 = part->value->value; + rc_evaluate_operand(&value, &part->value, NULL); if (value.value.u32 == 0) { /* null terminator - skip over remaining character macros */ while (part->next && part->next->display_type == RC_FORMAT_ASCIICHAR) @@ -725,7 +805,7 @@ static int rc_evaluate_richpresence_display(rc_richpresence_display_part_t* part value.type = RC_VALUE_TYPE_UNSIGNED; do { - value.value.u32 = part->value->value; + rc_evaluate_operand(&value, &part->value, NULL); if (value.value.u32 == 0) { /* null terminator - skip over remaining character macros */ while (part->next && part->next->display_type == RC_FORMAT_UNICODECHAR) @@ -770,7 +850,7 @@ static int rc_evaluate_richpresence_display(rc_richpresence_display_part_t* part break; default: - rc_typed_value_from_memref_value(&value, part->value); + rc_evaluate_operand(&value, &part->value, NULL); chars = rc_format_typed_value(tmp, sizeof(tmp), &value, part->display_type); text = tmp; break; @@ -797,7 +877,7 @@ static int rc_evaluate_richpresence_display(rc_richpresence_display_part_t* part return (int)(ptr - buffer); } -int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, lua_State* L) { +int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, void* unused_L) { rc_richpresence_display_t* display; for (display = richpresence->first_display; display; display = display->next) { @@ -806,8 +886,8 @@ int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* bu return rc_evaluate_richpresence_display(display->display, buffer, buffersize); /* triggers with required hits will be updated in rc_update_richpresence */ - if (!display->trigger.has_required_hits) - rc_test_trigger(&display->trigger, peek, peek_ud, L); + if (!display->has_required_hits) + rc_test_trigger(&display->trigger, peek, peek_ud, unused_L); /* if we've found a valid condition, process it */ if (display->trigger.state == RC_TRIGGER_STATE_TRIGGERED) @@ -818,18 +898,19 @@ int rc_get_richpresence_display_string(rc_richpresence_t* richpresence, char* bu return 0; } -int rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, lua_State* L) { - rc_update_richpresence(richpresence, peek, peek_ud, L); - return rc_get_richpresence_display_string(richpresence, buffer, buffersize, peek, peek_ud, L); +int rc_evaluate_richpresence(rc_richpresence_t* richpresence, char* buffer, size_t buffersize, rc_peek_t peek, void* peek_ud, void* unused_L) { + rc_update_richpresence(richpresence, peek, peek_ud, unused_L); + return rc_get_richpresence_display_string(richpresence, buffer, buffersize, peek, peek_ud, unused_L); } -void rc_reset_richpresence(rc_richpresence_t* self) { +void rc_reset_richpresence_triggers(rc_richpresence_t* self) { rc_richpresence_display_t* display; - rc_value_t* variable; for (display = self->first_display; display; display = display->next) rc_reset_trigger(&display->trigger); - - for (variable = self->variables; variable; variable = variable->next) - rc_reset_value(variable); +} + +void rc_reset_richpresence(rc_richpresence_t* self) { + rc_reset_richpresence_triggers(self); + rc_reset_values(self->values); } diff --git a/deps/rcheevos/src/rcheevos/runtime.c b/deps/rcheevos/src/rcheevos/runtime.c index 292b21b5d9..ec308d6d13 100644 --- a/deps/rcheevos/src/rcheevos/runtime.c +++ b/deps/rcheevos/src/rcheevos/runtime.c @@ -9,8 +9,34 @@ #define RC_RICHPRESENCE_DISPLAY_BUFFER_SIZE 256 +/* ===== natvis extensions ===== */ + +typedef struct __rc_runtime_trigger_list_t { rc_runtime_t runtime; } __rc_runtime_trigger_list_t; +typedef struct __rc_runtime_lboard_list_t { rc_runtime_t runtime; } __rc_runtime_lboard_list_t; + +static void rc_runtime_natvis_helper(const rc_runtime_event_t* runtime_event) +{ + struct natvis_extensions { + __rc_runtime_trigger_list_t trigger_list; + __rc_runtime_lboard_list_t lboard_list; + } natvis; + + memset(&natvis, 0, sizeof(natvis)); + (void)runtime_event; + + natvis.lboard_list.runtime.lboard_count = 0; +} + +/* ============================= */ + rc_runtime_t* rc_runtime_alloc(void) { - rc_runtime_t* self = malloc(sizeof(rc_runtime_t)); + rc_runtime_t* self; + + /* create a reference to rc_runtime_natvis_helper so the extensions get compiled in. */ + rc_runtime_event_handler_t unused = &rc_runtime_natvis_helper; + (void)unused; + + self = malloc(sizeof(rc_runtime_t)); if (self) { rc_runtime_init(self); @@ -22,16 +48,19 @@ rc_runtime_t* rc_runtime_alloc(void) { void rc_runtime_init(rc_runtime_t* self) { memset(self, 0, sizeof(rc_runtime_t)); - self->next_memref = &self->memrefs; - self->next_variable = &self->variables; + + self->memrefs = (rc_memrefs_t*)malloc(sizeof(*self->memrefs)); + rc_memrefs_init(self->memrefs); } void rc_runtime_destroy(rc_runtime_t* self) { uint32_t i; if (self->triggers) { - for (i = 0; i < self->trigger_count; ++i) - free(self->triggers[i].buffer); + for (i = 0; i < self->trigger_count; ++i) { + if (self->triggers[i].buffer) + free(self->triggers[i].buffer); + } free(self->triggers); self->triggers = NULL; @@ -40,8 +69,10 @@ void rc_runtime_destroy(rc_runtime_t* self) { } if (self->lboards) { - for (i = 0; i < self->lboard_count; ++i) - free(self->lboards[i].buffer); + for (i = 0; i < self->lboard_count; ++i) { + if (self->lboards[i].buffer) + free(self->lboards[i].buffer); + } free(self->lboards); self->lboards = NULL; @@ -49,20 +80,17 @@ void rc_runtime_destroy(rc_runtime_t* self) { self->lboard_count = self->lboard_capacity = 0; } - while (self->richpresence) { - rc_runtime_richpresence_t* previous = self->richpresence->previous; - - free(self->richpresence->buffer); + if (self->richpresence) { + if (self->richpresence->buffer) + free(self->richpresence->buffer); free(self->richpresence); - self->richpresence = previous; } - self->next_memref = 0; - self->memrefs = 0; + if (self->memrefs) + rc_memrefs_destroy(self->memrefs); - if (self->owns_self) { + if (self->owns_self) free(self); - } } void rc_runtime_checksum(const char* memaddr, uint8_t* md5) { @@ -72,45 +100,12 @@ void rc_runtime_checksum(const char* memaddr, uint8_t* md5) { md5_finish(&state, md5); } -static char rc_runtime_allocated_memrefs(rc_runtime_t* self) { - char owns_memref = 0; - - /* if at least one memref was allocated within the object, we can't free the buffer when the object is deactivated */ - if (*self->next_memref != NULL) { - owns_memref = 1; - /* advance through the new memrefs so we're ready for the next allocation */ - do { - self->next_memref = &(*self->next_memref)->next; - } while (*self->next_memref != NULL); - } - - /* if at least one variable was allocated within the object, we can't free the buffer when the object is deactivated */ - if (*self->next_variable != NULL) { - owns_memref = 1; - /* advance through the new variables so we're ready for the next allocation */ - do { - self->next_variable = &(*self->next_variable)->next; - } while (*self->next_variable != NULL); - } - - return owns_memref; -} - static void rc_runtime_deactivate_trigger_by_index(rc_runtime_t* self, uint32_t index) { - if (self->triggers[index].owns_memrefs) { - /* if the trigger has one or more memrefs in its buffer, we can't free the buffer. - * just null out the trigger so the runtime processor will skip it - */ - rc_reset_trigger(self->triggers[index].trigger); - self->triggers[index].trigger = NULL; - } - else { - /* trigger doesn't own any memrefs, go ahead and free it, then replace it with the last trigger */ - free(self->triggers[index].buffer); + /* free the trigger, then replace it with the last trigger */ + free(self->triggers[index].buffer); - if (--self->trigger_count > index) - memcpy(&self->triggers[index], &self->triggers[self->trigger_count], sizeof(rc_runtime_trigger_t)); - } + if (--self->trigger_count > index) + memcpy(&self->triggers[index], &self->triggers[self->trigger_count], sizeof(rc_runtime_trigger_t)); } void rc_runtime_deactivate_achievement(rc_runtime_t* self, uint32_t id) { @@ -122,15 +117,19 @@ void rc_runtime_deactivate_achievement(rc_runtime_t* self, uint32_t id) { } } -int rc_runtime_activate_achievement(rc_runtime_t* self, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx) { +int rc_runtime_activate_achievement(rc_runtime_t* self, uint32_t id, const char* memaddr, void* unused_L, int unused_funcs_idx) { void* trigger_buffer; rc_trigger_t* trigger; rc_runtime_trigger_t* runtime_trigger; - rc_parse_state_t parse; + rc_preparse_state_t preparse; + const char* preparse_memaddr = memaddr; uint8_t md5[16]; int32_t size; uint32_t i; + (void)unused_L; + (void)unused_funcs_idx; + if (memaddr == NULL) return RC_INVALID_MEMORY_OPERAND; @@ -169,7 +168,12 @@ int rc_runtime_activate_achievement(rc_runtime_t* self, uint32_t id, const char* } /* item has not been previously registered, determine how much space we need for it, and allocate it */ - size = rc_trigger_size(memaddr); + rc_init_preparse_state(&preparse); + preparse.parse.existing_memrefs = self->memrefs; + trigger = RC_ALLOC(rc_trigger_t, &preparse.parse); + rc_parse_trigger_internal(trigger, &preparse_memaddr, &preparse.parse); + + size = preparse.parse.offset; if (size < 0) return size; @@ -178,16 +182,15 @@ int rc_runtime_activate_achievement(rc_runtime_t* self, uint32_t id, const char* return RC_OUT_OF_MEMORY; /* populate the item, using the communal memrefs pool */ - rc_init_parse_state(&parse, trigger_buffer, L, funcs_idx); - parse.first_memref = &self->memrefs; - trigger = RC_ALLOC(rc_trigger_t, &parse); - rc_parse_trigger_internal(trigger, &memaddr, &parse); - rc_destroy_parse_state(&parse); + rc_reset_parse_state(&preparse.parse, trigger_buffer); + rc_preparse_reserve_memrefs(&preparse, self->memrefs); + trigger = RC_ALLOC(rc_trigger_t, &preparse.parse); + rc_parse_trigger_internal(trigger, &memaddr, &preparse.parse); + rc_destroy_preparse_state(&preparse); - if (parse.offset < 0) { + if (preparse.parse.offset < 0) { free(trigger_buffer); - *self->next_memref = NULL; /* disassociate any memrefs allocated by the failed parse */ - return parse.offset; + return preparse.parse.offset; } /* grow the trigger buffer if necessary */ @@ -200,7 +203,6 @@ int rc_runtime_activate_achievement(rc_runtime_t* self, uint32_t id, const char* if (!self->triggers) { free(trigger_buffer); - *self->next_memref = NULL; /* disassociate any memrefs allocated by the failed parse */ return RC_OUT_OF_MEMORY; } } @@ -213,11 +215,9 @@ int rc_runtime_activate_achievement(rc_runtime_t* self, uint32_t id, const char* runtime_trigger->invalid_memref = NULL; memcpy(runtime_trigger->md5, md5, 16); runtime_trigger->serialized_size = 0; - runtime_trigger->owns_memrefs = rc_runtime_allocated_memrefs(self); ++self->trigger_count; /* reset it, and return it */ - trigger->memrefs = NULL; rc_reset_trigger(trigger); return RC_OK; } @@ -285,20 +285,11 @@ int rc_runtime_format_achievement_measured(const rc_runtime_t* runtime, uint32_t } static void rc_runtime_deactivate_lboard_by_index(rc_runtime_t* self, uint32_t index) { - if (self->lboards[index].owns_memrefs) { - /* if the lboard has one or more memrefs in its buffer, we can't free the buffer. - * just null out the lboard so the runtime processor will skip it - */ - rc_reset_lboard(self->lboards[index].lboard); - self->lboards[index].lboard = NULL; - } - else { - /* lboard doesn't own any memrefs, go ahead and free it, then replace it with the last lboard */ - free(self->lboards[index].buffer); + /* free the lboard, then replace it with the last lboard */ + free(self->lboards[index].buffer); - if (--self->lboard_count > index) - memcpy(&self->lboards[index], &self->lboards[self->lboard_count], sizeof(rc_runtime_lboard_t)); - } + if (--self->lboard_count > index) + memcpy(&self->lboards[index], &self->lboards[self->lboard_count], sizeof(rc_runtime_lboard_t)); } void rc_runtime_deactivate_lboard(rc_runtime_t* self, uint32_t id) { @@ -310,15 +301,18 @@ void rc_runtime_deactivate_lboard(rc_runtime_t* self, uint32_t id) { } } -int rc_runtime_activate_lboard(rc_runtime_t* self, uint32_t id, const char* memaddr, lua_State* L, int funcs_idx) { +int rc_runtime_activate_lboard(rc_runtime_t* self, uint32_t id, const char* memaddr, void* unused_L, int unused_funcs_idx) { void* lboard_buffer; uint8_t md5[16]; rc_lboard_t* lboard; - rc_parse_state_t parse; + rc_preparse_state_t preparse; rc_runtime_lboard_t* runtime_lboard; - int32_t size; + int size; uint32_t i; + (void)unused_L; + (void)unused_funcs_idx; + if (memaddr == 0) return RC_INVALID_MEMORY_OPERAND; @@ -357,7 +351,12 @@ int rc_runtime_activate_lboard(rc_runtime_t* self, uint32_t id, const char* mema } /* item has not been previously registered, determine how much space we need for it, and allocate it */ - size = rc_lboard_size(memaddr); + rc_init_preparse_state(&preparse); + preparse.parse.existing_memrefs = self->memrefs; + lboard = RC_ALLOC(rc_lboard_t, &preparse.parse); + rc_parse_lboard_internal(lboard, memaddr, &preparse.parse); + + size = preparse.parse.offset; if (size < 0) return size; @@ -366,16 +365,15 @@ int rc_runtime_activate_lboard(rc_runtime_t* self, uint32_t id, const char* mema return RC_OUT_OF_MEMORY; /* populate the item, using the communal memrefs pool */ - rc_init_parse_state(&parse, lboard_buffer, L, funcs_idx); - lboard = RC_ALLOC(rc_lboard_t, &parse); - parse.first_memref = &self->memrefs; - rc_parse_lboard_internal(lboard, memaddr, &parse); - rc_destroy_parse_state(&parse); + rc_reset_parse_state(&preparse.parse, lboard_buffer); + rc_preparse_reserve_memrefs(&preparse, self->memrefs); + lboard = RC_ALLOC(rc_lboard_t, &preparse.parse); + rc_parse_lboard_internal(lboard, memaddr, &preparse.parse); + rc_destroy_preparse_state(&preparse); - if (parse.offset < 0) { + if (preparse.parse.offset < 0) { free(lboard_buffer); - *self->next_memref = NULL; /* disassociate any memrefs allocated by the failed parse */ - return parse.offset; + return preparse.parse.offset; } /* grow the lboard buffer if necessary */ @@ -388,7 +386,6 @@ int rc_runtime_activate_lboard(rc_runtime_t* self, uint32_t id, const char* mema if (!self->lboards) { free(lboard_buffer); - *self->next_memref = NULL; /* disassociate any memrefs allocated by the failed parse */ return RC_OUT_OF_MEMORY; } } @@ -402,10 +399,8 @@ int rc_runtime_activate_lboard(rc_runtime_t* self, uint32_t id, const char* mema runtime_lboard->invalid_memref = NULL; memcpy(runtime_lboard->md5, md5, 16); runtime_lboard->serialized_size = 0; - runtime_lboard->owns_memrefs = rc_runtime_allocated_memrefs(self); /* reset it, and return it */ - lboard->memrefs = NULL; rc_reset_lboard(lboard); return RC_OK; } @@ -427,61 +422,44 @@ int rc_runtime_format_lboard_value(char* buffer, int size, int32_t value, int fo return rc_format_value(buffer, size, value, format); } -int rc_runtime_activate_richpresence(rc_runtime_t* self, const char* script, lua_State* L, int funcs_idx) { +int rc_runtime_activate_richpresence(rc_runtime_t* self, const char* script, void* unused_L, int unused_funcs_idx) { rc_richpresence_t* richpresence; - rc_runtime_richpresence_t* previous; - rc_runtime_richpresence_t** previous_ptr; - rc_parse_state_t parse; + rc_preparse_state_t preparse; uint8_t md5[16]; int size; + (void)unused_L; + (void)unused_funcs_idx; + if (script == NULL) return RC_MISSING_DISPLAY_STRING; rc_runtime_checksum(script, md5); /* look for existing match */ - previous_ptr = NULL; - previous = self->richpresence; - while (previous) { - if (previous && self->richpresence->richpresence && memcmp(self->richpresence->md5, md5, 16) == 0) { - /* unchanged. reset all of the conditions */ - rc_reset_richpresence(self->richpresence->richpresence); + if (self->richpresence && self->richpresence->richpresence && memcmp(self->richpresence->md5, md5, 16) == 0) { + /* unchanged. reset all of the conditions */ + rc_reset_richpresence(self->richpresence->richpresence); - /* move to front of linked list*/ - if (previous_ptr) { - *previous_ptr = previous->previous; - if (!self->richpresence->owns_memrefs) { - free(self->richpresence->buffer); - previous->previous = self->richpresence->previous; - } - else { - previous->previous = self->richpresence; - } - - self->richpresence = previous; - } - - /* return success*/ - return RC_OK; - } - - previous_ptr = &previous->previous; - previous = previous->previous; + /* return success*/ + return RC_OK; } /* no existing match found, parse script */ - size = rc_richpresence_size(script); + rc_init_preparse_state(&preparse); + preparse.parse.existing_memrefs = self->memrefs; + richpresence = RC_ALLOC(rc_richpresence_t, &preparse.parse); + preparse.parse.variables = &richpresence->values; + rc_parse_richpresence_internal(richpresence, script, &preparse.parse); + + size = preparse.parse.offset; if (size < 0) return size; - /* if the previous script doesn't have any memrefs, free it */ - previous = self->richpresence; - if (previous) { - if (!previous->owns_memrefs) { - free(previous->buffer); - previous = previous->previous; - } + /* if there's a previous script, free it */ + if (self->richpresence) { + free(self->richpresence->buffer); + free(self->richpresence); } /* allocate and process the new script */ @@ -489,34 +467,26 @@ int rc_runtime_activate_richpresence(rc_runtime_t* self, const char* script, lua if (!self->richpresence) return RC_OUT_OF_MEMORY; - self->richpresence->previous = previous; - self->richpresence->owns_memrefs = 0; memcpy(self->richpresence->md5, md5, sizeof(md5)); - self->richpresence->buffer = malloc(size); + self->richpresence->buffer = malloc(size); if (!self->richpresence->buffer) return RC_OUT_OF_MEMORY; - rc_init_parse_state(&parse, self->richpresence->buffer, L, funcs_idx); - self->richpresence->richpresence = richpresence = RC_ALLOC(rc_richpresence_t, &parse); - parse.first_memref = &self->memrefs; - parse.variables = &self->variables; - rc_parse_richpresence_internal(richpresence, script, &parse); - rc_destroy_parse_state(&parse); + rc_reset_parse_state(&preparse.parse, self->richpresence->buffer); + rc_preparse_reserve_memrefs(&preparse, self->memrefs); + richpresence = RC_ALLOC(rc_richpresence_t, &preparse.parse); + preparse.parse.variables = &richpresence->values; + rc_parse_richpresence_internal(richpresence, script, &preparse.parse); + rc_destroy_preparse_state(&preparse); - if (parse.offset < 0) { + if (preparse.parse.offset < 0) { free(self->richpresence->buffer); free(self->richpresence); - self->richpresence = previous; - *self->next_memref = NULL; /* disassociate any memrefs allocated by the failed parse */ - return parse.offset; + self->richpresence = NULL; + return preparse.parse.offset; } - self->richpresence->owns_memrefs = rc_runtime_allocated_memrefs(self); - - richpresence->memrefs = NULL; - richpresence->variables = NULL; - if (!richpresence->first_display || !richpresence->first_display->display) { /* non-existant rich presence */ self->richpresence->richpresence = NULL; @@ -524,27 +494,27 @@ int rc_runtime_activate_richpresence(rc_runtime_t* self, const char* script, lua else { /* reset all of the conditions */ rc_reset_richpresence(richpresence); + self->richpresence->richpresence = richpresence; } return RC_OK; } -int rc_runtime_get_richpresence(const rc_runtime_t* self, char* buffer, size_t buffersize, rc_runtime_peek_t peek, void* peek_ud, lua_State* L) { +int rc_runtime_get_richpresence(const rc_runtime_t* self, char* buffer, size_t buffersize, rc_runtime_peek_t peek, void* peek_ud, void* unused_L) { if (self->richpresence && self->richpresence->richpresence) - return rc_get_richpresence_display_string(self->richpresence->richpresence, buffer, buffersize, peek, peek_ud, L); + return rc_get_richpresence_display_string(self->richpresence->richpresence, buffer, buffersize, peek, peek_ud, unused_L); *buffer = '\0'; return 0; } -void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, lua_State* L) { +void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_handler, rc_runtime_peek_t peek, void* ud, void* unused_L) { rc_runtime_event_t runtime_event; int i; runtime_event.value = 0; rc_update_memref_values(self->memrefs, peek, ud); - rc_update_variables(self->variables, peek, ud, L); for (i = self->trigger_count - 1; i >= 0; --i) { rc_trigger_t* trigger = self->triggers[i].trigger; @@ -570,7 +540,7 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha old_measured_value = trigger->measured_value; old_state = trigger->state; - new_state = rc_evaluate_trigger(trigger, peek, ud, L); + new_state = rc_evaluate_trigger(trigger, peek, ud, unused_L); /* trigger->state doesn't actually change to RESET, RESET just serves as a notification. * handle the notification, then look at the actual state */ @@ -672,7 +642,7 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha } lboard_state = lboard->state; - switch (rc_evaluate_lboard(lboard, &runtime_event.value, peek, ud, L)) + switch (rc_evaluate_lboard(lboard, &runtime_event.value, peek, ud, unused_L)) { case RC_LBOARD_STATE_STARTED: /* leaderboard is running */ if (lboard_state != RC_LBOARD_STATE_STARTED) { @@ -710,11 +680,10 @@ void rc_runtime_do_frame(rc_runtime_t* self, rc_runtime_event_handler_t event_ha } if (self->richpresence && self->richpresence->richpresence) - rc_update_richpresence(self->richpresence->richpresence, peek, ud, L); + rc_update_richpresence(self->richpresence->richpresence, peek, ud, unused_L); } void rc_runtime_reset(rc_runtime_t* self) { - rc_value_t* variable; uint32_t i; for (i = 0; i < self->trigger_count; ++i) { @@ -729,9 +698,6 @@ void rc_runtime_reset(rc_runtime_t* self) { if (self->richpresence && self->richpresence->richpresence) rc_reset_richpresence(self->richpresence->richpresence); - - for (variable = self->variables; variable; variable = variable->next) - rc_reset_value(variable); } static int rc_condset_contains_memref(const rc_condset_t* condset, const rc_memref_t* memref) { @@ -815,47 +781,41 @@ static void rc_runtime_invalidate_memref(rc_runtime_t* self, rc_memref_t* memref } void rc_runtime_invalidate_address(rc_runtime_t* self, uint32_t address) { - rc_memref_t** last_memref = &self->memrefs; - rc_memref_t* memref = self->memrefs; + rc_memref_list_t* memref_list = &self->memrefs->memrefs; + do { + rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_stop = memref + memref_list->count; - while (memref) { - if (memref->address == address && !memref->value.is_indirect) { - /* remove the invalid memref from the chain so we don't try to evaluate it in the future. - * it's still there, so anything referencing it will continue to fetch 0. - */ - *last_memref = memref->next; - - rc_runtime_invalidate_memref(self, memref); - break; + for (; memref < memref_stop; ++memref) { + if (memref->address == address) { + memref->value.type = RC_VALUE_TYPE_NONE; + rc_runtime_invalidate_memref(self, memref); + } } - last_memref = &memref->next; - memref = *last_memref; - } + memref_list = memref_list->next; + } while (memref_list); } -void rc_runtime_validate_addresses(rc_runtime_t* self, rc_runtime_event_handler_t event_handler, +void rc_runtime_validate_addresses(rc_runtime_t* self, rc_runtime_event_handler_t event_handler, rc_runtime_validate_address_t validate_handler) { - rc_memref_t** last_memref = &self->memrefs; - rc_memref_t* memref = self->memrefs; int num_invalid = 0; + rc_memref_list_t* memref_list = &self->memrefs->memrefs; + do { + rc_memref_t* memref = memref_list->items; + const rc_memref_t* memref_stop = memref + memref_list->count; - while (memref) { - if (!memref->value.is_indirect && !validate_handler(memref->address)) { - /* remove the invalid memref from the chain so we don't try to evaluate it in the future. - * it's still there, so anything referencing it will continue to fetch 0. - */ - *last_memref = memref->next; + for (; memref < memref_stop; ++memref) { + if (!validate_handler(memref->address)) { + memref->value.type = RC_VALUE_TYPE_NONE; + rc_runtime_invalidate_memref(self, memref); - rc_runtime_invalidate_memref(self, memref); - ++num_invalid; - } - else { - last_memref = &memref->next; + ++num_invalid; + } } - memref = *last_memref; - } + memref_list = memref_list->next; + } while (memref_list); if (num_invalid) { rc_runtime_event_t runtime_event; diff --git a/deps/rcheevos/src/rcheevos/runtime_progress.c b/deps/rcheevos/src/rcheevos/runtime_progress.c index 629f0e376f..f46c2ea911 100644 --- a/deps/rcheevos/src/rcheevos/runtime_progress.c +++ b/deps/rcheevos/src/rcheevos/runtime_progress.c @@ -28,8 +28,6 @@ typedef struct rc_runtime_progress_t { uint32_t buffer_size; uint32_t chunk_size_offset; - - lua_State* L; } rc_runtime_progress_t; #define assert_chunk_size(expected_size) assert((uint32_t)(progress->offset - progress->chunk_size_offset - 4) == (uint32_t)(expected_size)) @@ -40,7 +38,7 @@ typedef struct rc_runtime_progress_t { #define RC_VAR_FLAG_HAS_COND_DATA 0x01000000 -#define RC_COND_FLAG_IS_TRUE 0x00000001 +#define RC_COND_FLAG_IS_TRUE_MASK 0x00000003 #define RC_COND_FLAG_OPERAND1_IS_INDIRECT_MEMREF 0x00010000 #define RC_COND_FLAG_OPERAND1_MEMREF_CHANGED_THIS_FRAME 0x00020000 #define RC_COND_FLAG_OPERAND2_IS_INDIRECT_MEMREF 0x00100000 @@ -116,22 +114,17 @@ static void rc_runtime_progress_end_chunk(rc_runtime_progress_t* progress) } } -static void rc_runtime_progress_init(rc_runtime_progress_t* progress, const rc_runtime_t* runtime, lua_State* L) +static void rc_runtime_progress_init(rc_runtime_progress_t* progress, const rc_runtime_t* runtime) { memset(progress, 0, sizeof(rc_runtime_progress_t)); progress->runtime = runtime; - progress->L = L; } #define RC_RUNTIME_SERIALIZED_MEMREF_SIZE 16 /* 4x uint: address, flags, value, prior */ static int rc_runtime_progress_write_memrefs(rc_runtime_progress_t* progress) { - rc_memref_t* memref; - uint32_t count = 0; - - for (memref = progress->runtime->memrefs; memref; memref = memref->next) - ++count; + uint32_t count = rc_memrefs_count_memrefs(progress->runtime->memrefs); if (count == 0) return RC_OK; @@ -145,15 +138,24 @@ static int rc_runtime_progress_write_memrefs(rc_runtime_progress_t* progress) } else { uint32_t flags = 0; - for (memref = progress->runtime->memrefs; memref; memref = memref->next) { - flags = memref->value.size; - if (memref->value.changed) - flags |= RC_MEMREF_FLAG_CHANGED_THIS_FRAME; + const rc_memref_list_t* memref_list = &progress->runtime->memrefs->memrefs; + const rc_memref_t* memref; - rc_runtime_progress_write_uint(progress, memref->address); - rc_runtime_progress_write_uint(progress, flags); - rc_runtime_progress_write_uint(progress, memref->value.value); - rc_runtime_progress_write_uint(progress, memref->value.prior); + for (; memref_list; memref_list = memref_list->next) { + const rc_memref_t* memref_end; + + memref = memref_list->items; + memref_end = memref + memref_list->count; + for (; memref < memref_end; ++memref) { + flags = memref->value.size; + if (memref->value.changed) + flags |= RC_MEMREF_FLAG_CHANGED_THIS_FRAME; + + rc_runtime_progress_write_uint(progress, memref->address); + rc_runtime_progress_write_uint(progress, flags); + rc_runtime_progress_write_uint(progress, memref->value.value); + rc_runtime_progress_write_uint(progress, memref->value.prior); + } } } @@ -162,13 +164,77 @@ static int rc_runtime_progress_write_memrefs(rc_runtime_progress_t* progress) return RC_OK; } +static void rc_runtime_progress_update_modified_memrefs(rc_runtime_progress_t* progress) +{ + rc_typed_value_t value, prior_value, modifier, prior_modifier; + rc_modified_memref_list_t* modified_memref_list; + rc_modified_memref_t* modified_memref; + rc_operand_t prior_parent_operand, prior_modifier_operand; + rc_memref_t prior_parent_memref, prior_modifier_memref; + + modified_memref_list = &progress->runtime->memrefs->modified_memrefs; + for (; modified_memref_list; modified_memref_list = modified_memref_list->next) { + const rc_modified_memref_t* modified_memref_end; + modified_memref = modified_memref_list->items; + modified_memref_end = modified_memref + modified_memref_list->count; + for (; modified_memref < modified_memref_end; ++modified_memref) { + modified_memref->memref.value.changed = 0; + + /* indirect memref values are stored in conditions */ + if (modified_memref->modifier_type == RC_OPERATOR_INDIRECT_READ) + continue; + + /* non-indirect memref values can be reconstructed from the parents */ + memcpy(&prior_parent_operand, &modified_memref->parent, sizeof(prior_parent_operand)); + if (rc_operand_is_memref(&prior_parent_operand)) { + memcpy(&prior_parent_memref, modified_memref->parent.value.memref, sizeof(prior_parent_memref)); + prior_parent_memref.value.value = prior_parent_memref.value.prior; + modified_memref->memref.value.changed |= prior_parent_memref.value.changed; + prior_parent_operand.value.memref = &prior_parent_memref; + } + + memcpy(&prior_modifier_operand, &modified_memref->modifier, sizeof(prior_modifier_operand)); + if (rc_operand_is_memref(&prior_modifier_operand)) { + memcpy(&prior_modifier_memref, modified_memref->modifier.value.memref, sizeof(prior_modifier_memref)); + prior_modifier_memref.value.value = prior_modifier_memref.value.prior; + modified_memref->memref.value.changed |= prior_modifier_memref.value.changed; + prior_modifier_operand.value.memref = &prior_modifier_memref; + } + + rc_evaluate_operand(&value, &modified_memref->parent, NULL); + rc_evaluate_operand(&modifier, &modified_memref->modifier, NULL); + rc_evaluate_operand(&prior_value, &prior_parent_operand, NULL); + rc_evaluate_operand(&prior_modifier, &prior_modifier_operand, NULL); + + if (modified_memref->modifier_type == RC_OPERATOR_SUB_PARENT) { + rc_typed_value_negate(&value); + rc_typed_value_add(&value, &modifier); + + rc_typed_value_negate(&prior_value); + rc_typed_value_add(&prior_value, &prior_modifier); + } + else { + rc_typed_value_combine(&value, &modifier, modified_memref->modifier_type); + rc_typed_value_combine(&prior_value, &prior_modifier, modified_memref->modifier_type); + } + + rc_typed_value_convert(&value, modified_memref->memref.value.type); + modified_memref->memref.value.value = value.value.u32; + + rc_typed_value_convert(&prior_value, modified_memref->memref.value.type); + modified_memref->memref.value.prior = prior_value.value.u32; + } + } +} + static int rc_runtime_progress_read_memrefs(rc_runtime_progress_t* progress) { uint32_t entries; uint32_t address, flags, value, prior; uint8_t size; + rc_memref_list_t* unmatched_memref_list = &progress->runtime->memrefs->memrefs; + rc_memref_t* first_unmatched_memref = unmatched_memref_list->items; rc_memref_t* memref; - rc_memref_t* first_unmatched_memref = progress->runtime->memrefs; /* re-read the chunk size to determine how many memrefs are present */ progress->offset -= 4; @@ -183,24 +249,46 @@ static int rc_runtime_progress_read_memrefs(rc_runtime_progress_t* progress) size = flags & 0xFF; memref = first_unmatched_memref; - while (memref) { - if (memref->address == address && memref->value.size == size) { - memref->value.value = value; - memref->value.changed = (flags & RC_MEMREF_FLAG_CHANGED_THIS_FRAME) ? 1 : 0; - memref->value.prior = prior; + if (memref->address == address && memref->value.size == size) { + memref->value.value = value; + memref->value.changed = (flags & RC_MEMREF_FLAG_CHANGED_THIS_FRAME) ? 1 : 0; + memref->value.prior = prior; - if (memref == first_unmatched_memref) - first_unmatched_memref = memref->next; - - break; + first_unmatched_memref++; + if (first_unmatched_memref >= unmatched_memref_list->items + unmatched_memref_list->count) { + unmatched_memref_list = unmatched_memref_list->next; + if (!unmatched_memref_list) + break; + first_unmatched_memref = unmatched_memref_list->items; } + } + else { + rc_memref_list_t* memref_list = unmatched_memref_list; + do { + ++memref; + if (memref >= memref_list->items + memref_list->count) { + memref_list = memref_list->next; + if (!memref_list) + break; - memref = memref->next; + memref = memref_list->items; + } + + if (memref->address == address && memref->value.size == size) { + memref->value.value = value; + memref->value.changed = (flags & RC_MEMREF_FLAG_CHANGED_THIS_FRAME) ? 1 : 0; + memref->value.prior = prior; + break; + } + + } while (1); } --entries; } + rc_runtime_progress_update_modified_memrefs(progress); + return RC_OK; } @@ -211,11 +299,14 @@ static int rc_runtime_progress_is_indirect_memref(rc_operand_t* oper) case RC_OPERAND_CONST: case RC_OPERAND_FP: case RC_OPERAND_RECALL: - case RC_OPERAND_LUA: + case RC_OPERAND_FUNC: return 0; default: - return oper->value.memref->value.is_indirect; + if (oper->value.memref->value.memref_type != RC_MEMREF_TYPE_MODIFIED_MEMREF) + return 0; + + return ((const rc_modified_memref_t*)oper->value.memref)->modifier_type == RC_OPERATOR_INDIRECT_READ; } } @@ -231,9 +322,7 @@ static int rc_runtime_progress_write_condset(rc_runtime_progress_t* progress, rc cond = condset->conditions; while (cond) { - flags = 0; - if (cond->is_true) - flags |= RC_COND_FLAG_IS_TRUE; + flags = (cond->is_true & RC_COND_FLAG_IS_TRUE_MASK); if (rc_runtime_progress_is_indirect_memref(&cond->operand1)) { flags |= RC_COND_FLAG_OPERAND1_IS_INDIRECT_MEMREF; @@ -287,7 +376,7 @@ static int rc_runtime_progress_read_condset(rc_runtime_progress_t* progress, rc_ cond->current_hits = rc_runtime_progress_read_uint(progress); flags = rc_runtime_progress_read_uint(progress); - cond->is_true = (flags & RC_COND_FLAG_IS_TRUE) ? 1 : 0; + cond->is_true = (flags & RC_COND_FLAG_IS_TRUE_MASK); if (flags & RC_COND_FLAG_OPERAND1_IS_INDIRECT_MEMREF) { if (!rc_operand_is_memref(&cond->operand1)) /* this should never happen, but better safe than sorry */ @@ -317,8 +406,8 @@ static uint32_t rc_runtime_progress_should_serialize_variable_condset(const rc_c { const rc_condition_t* condition; - /* predetermined presence of pause flag or indirect memrefs - must serialize */ - if (conditions->has_pause || conditions->has_indirect_memrefs) + /* predetermined presence of pause flag - must serialize */ + if (conditions->has_pause) return RC_VAR_FLAG_HAS_COND_DATA; /* if any conditions has required hits, must serialize */ @@ -358,12 +447,15 @@ static int rc_runtime_progress_write_variable(rc_runtime_progress_t* progress, c static int rc_runtime_progress_write_variables(rc_runtime_progress_t* progress) { - uint32_t count = 0; - const rc_value_t* variable; + uint32_t count; + const rc_value_t* value; int result; - for (variable = progress->runtime->variables; variable; variable = variable->next) - ++count; + if (!progress->runtime->richpresence || !progress->runtime->richpresence->richpresence) + return RC_OK; + + value = progress->runtime->richpresence->richpresence->values; + count = rc_count_values(value); if (count == 0) return RC_OK; @@ -374,14 +466,14 @@ static int rc_runtime_progress_write_variables(rc_runtime_progress_t* progress) rc_runtime_progress_start_chunk(progress, RC_RUNTIME_CHUNK_VARIABLES); rc_runtime_progress_write_uint(progress, count); - for (variable = progress->runtime->variables; variable; variable = variable->next) { - uint32_t djb2 = rc_djb2(variable->name); + for (; value; value = value->next) { + const uint32_t djb2 = rc_djb2(value->name); if (progress->offset + 16 > progress->buffer_size) return RC_INSUFFICIENT_BUFFER; rc_runtime_progress_write_uint(progress, djb2); - result = rc_runtime_progress_write_variable(progress, variable); + result = rc_runtime_progress_write_variable(progress, value); if (result != RC_OK) return result; } @@ -418,19 +510,20 @@ static int rc_runtime_progress_read_variables(rc_runtime_progress_t* progress) }; struct rc_pending_value_t local_pending_variables[32]; struct rc_pending_value_t* pending_variables; - rc_value_t* variable; + rc_value_t* value; uint32_t count, serialized_count; int result; - uint32_t i; + int32_t i; serialized_count = rc_runtime_progress_read_uint(progress); if (serialized_count == 0) return RC_OK; - count = 0; - for (variable = progress->runtime->variables; variable; variable = variable->next) - ++count; + if (!progress->runtime->richpresence || !progress->runtime->richpresence->richpresence) + return RC_OK; + value = progress->runtime->richpresence->richpresence->values; + count = rc_count_values(value); if (count == 0) return RC_OK; @@ -443,22 +536,22 @@ static int rc_runtime_progress_read_variables(rc_runtime_progress_t* progress) return RC_OUT_OF_MEMORY; } - count = 0; - for (variable = progress->runtime->variables; variable; variable = variable->next) { - pending_variables[count].variable = variable; - pending_variables[count].djb2 = rc_djb2(variable->name); - ++count; + i = (int32_t)count; + for (; value; value = value->next) { + --i; + pending_variables[i].variable = value; + pending_variables[i].djb2 = rc_djb2(value->name); } result = RC_OK; for (; serialized_count > 0 && result == RC_OK; --serialized_count) { uint32_t djb2 = rc_runtime_progress_read_uint(progress); - for (i = 0; i < count; ++i) { + for (i = (int32_t)count - 1; i >= 0; --i) { if (pending_variables[i].djb2 == djb2) { - variable = pending_variables[i].variable; - result = rc_runtime_progress_read_variable(progress, variable); + value = pending_variables[i].variable; + result = rc_runtime_progress_read_variable(progress, value); if (result == RC_OK) { - if (i < count - 1) + if (i < (int32_t)count - 1) memcpy(&pending_variables[i], &pending_variables[count - 1], sizeof(struct rc_pending_value_t)); count--; } @@ -467,8 +560,17 @@ static int rc_runtime_progress_read_variables(rc_runtime_progress_t* progress) } } + /* VS raises a C6385 warning here because it thinks count can exceed the size of the local_pending_variables array. + * When count is larger, pending_variables points to allocated memory, so the warning is wrong. */ +#if defined (_MSC_VER) + #pragma warning(push) + #pragma warning(disable:6385) +#endif while (count > 0) rc_reset_value(pending_variables[--count].variable); +#if defined (_MSC_VER) + #pragma warning(pop) +#endif if (pending_variables != local_pending_variables) free(pending_variables); @@ -742,7 +844,7 @@ static int rc_runtime_progress_read_rich_presence(rc_runtime_progress_t* progres return RC_OK; if (!rc_runtime_progress_match_md5(progress, progress->runtime->richpresence->md5)) { - rc_reset_richpresence(progress->runtime->richpresence->richpresence); + rc_reset_richpresence_triggers(progress->runtime->richpresence->richpresence); return RC_OK; } @@ -799,12 +901,14 @@ static int rc_runtime_progress_serialize_internal(rc_runtime_progress_t* progres return RC_OK; } -uint32_t rc_runtime_progress_size(const rc_runtime_t* runtime, lua_State* L) +uint32_t rc_runtime_progress_size(const rc_runtime_t* runtime, void* unused_L) { rc_runtime_progress_t progress; int result; - rc_runtime_progress_init(&progress, runtime, L); + (void)unused_L; + + rc_runtime_progress_init(&progress, runtime); progress.buffer_size = 0xFFFFFFFF; result = rc_runtime_progress_serialize_internal(&progress); @@ -814,31 +918,33 @@ uint32_t rc_runtime_progress_size(const rc_runtime_t* runtime, lua_State* L) return progress.offset; } -int rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, lua_State* L) +int rc_runtime_serialize_progress(void* buffer, const rc_runtime_t* runtime, void* unused_L) { - return rc_runtime_serialize_progress_sized(buffer, 0xFFFFFFFF, runtime, L); + return rc_runtime_serialize_progress_sized(buffer, 0xFFFFFFFF, runtime, unused_L); } -int rc_runtime_serialize_progress_sized(uint8_t* buffer, uint32_t buffer_size, const rc_runtime_t* runtime, lua_State* L) +int rc_runtime_serialize_progress_sized(uint8_t* buffer, uint32_t buffer_size, const rc_runtime_t* runtime, void* unused_L) { rc_runtime_progress_t progress; + (void)unused_L; + if (!buffer) return RC_INVALID_STATE; - rc_runtime_progress_init(&progress, runtime, L); + rc_runtime_progress_init(&progress, runtime); progress.buffer = (uint8_t*)buffer; progress.buffer_size = buffer_size; return rc_runtime_progress_serialize_internal(&progress); } -int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const uint8_t* serialized, lua_State* L) +int rc_runtime_deserialize_progress(rc_runtime_t* runtime, const uint8_t* serialized, void* unused_L) { - return rc_runtime_deserialize_progress_sized(runtime, serialized, 0xFFFFFFFF, L); + return rc_runtime_deserialize_progress_sized(runtime, serialized, 0xFFFFFFFF, unused_L); } -int rc_runtime_deserialize_progress_sized(rc_runtime_t* runtime, const uint8_t* serialized, uint32_t serialized_size, lua_State* L) +int rc_runtime_deserialize_progress_sized(rc_runtime_t* runtime, const uint8_t* serialized, uint32_t serialized_size, void* unused_L) { rc_runtime_progress_t progress; md5_state_t state; @@ -850,12 +956,14 @@ int rc_runtime_deserialize_progress_sized(rc_runtime_t* runtime, const uint8_t* int seen_rich_presence = 0; int result = RC_OK; + (void)unused_L; + if (!serialized || serialized_size < RC_RUNTIME_MIN_BUFFER_SIZE) { rc_runtime_reset(runtime); return RC_INSUFFICIENT_BUFFER; } - rc_runtime_progress_init(&progress, runtime, L); + rc_runtime_progress_init(&progress, runtime); progress.buffer = (uint8_t*)serialized; if (rc_runtime_progress_read_uint(&progress) != RC_RUNTIME_MARKER) { @@ -958,7 +1066,7 @@ int rc_runtime_deserialize_progress_sized(rc_runtime_t* runtime, const uint8_t* } if (!seen_rich_presence && runtime->richpresence && runtime->richpresence->richpresence) - rc_reset_richpresence(runtime->richpresence->richpresence); + rc_reset_richpresence_triggers(runtime->richpresence->richpresence); } return result; diff --git a/deps/rcheevos/src/rcheevos/trigger.c b/deps/rcheevos/src/rcheevos/trigger.c index 71e186c014..3a37d98368 100644 --- a/deps/rcheevos/src/rcheevos/trigger.c +++ b/deps/rcheevos/src/rcheevos/trigger.c @@ -16,21 +16,20 @@ void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_pars parse->measured_as_percent = 0; if (*aux == 's' || *aux == 'S') { - self->requirement = 0; + self->requirement = NULL; } else { - self->requirement = rc_parse_condset(&aux, parse, 0); + self->requirement = rc_parse_condset(&aux, parse); - if (parse->offset < 0) { + if (parse->offset < 0) return; - } - self->requirement->next = 0; + self->requirement->next = NULL; } while (*aux == 's' || *aux == 'S') { aux++; - *next = rc_parse_condset(&aux, parse, 0); + *next = rc_parse_condset(&aux, parse); if (parse->offset < 0) { return; @@ -39,7 +38,7 @@ void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_pars next = &(*next)->next; } - *next = 0; + *next = NULL; *memaddr = aux; self->measured_target = parse->measured_target; @@ -47,39 +46,49 @@ void rc_parse_trigger_internal(rc_trigger_t* self, const char** memaddr, rc_pars self->measured_as_percent = parse->measured_as_percent; self->state = RC_TRIGGER_STATE_WAITING; self->has_hits = 0; - self->has_required_hits = parse->has_required_hits; + self->has_memrefs = 0; } int rc_trigger_size(const char* memaddr) { - rc_trigger_t* self; - rc_parse_state_t parse; - rc_memref_t* memrefs; - rc_init_parse_state(&parse, 0, 0, 0); - rc_init_parse_state_memrefs(&parse, &memrefs); + rc_trigger_with_memrefs_t* trigger; + rc_preparse_state_t preparse; + rc_init_preparse_state(&preparse); - self = RC_ALLOC(rc_trigger_t, &parse); - rc_parse_trigger_internal(self, &memaddr, &parse); + trigger = RC_ALLOC(rc_trigger_with_memrefs_t, &preparse.parse); + rc_parse_trigger_internal(&trigger->trigger, &memaddr, &preparse.parse); + rc_preparse_alloc_memrefs(NULL, &preparse); - rc_destroy_parse_state(&parse); - return parse.offset; + rc_destroy_preparse_state(&preparse); + return preparse.parse.offset; } -rc_trigger_t* rc_parse_trigger(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx) { - rc_trigger_t* self; - rc_parse_state_t parse; +rc_trigger_t* rc_parse_trigger(void* buffer, const char* memaddr, void* unused_L, int unused_funcs_idx) { + rc_trigger_with_memrefs_t* trigger; + rc_preparse_state_t preparse; + const char* preparse_memaddr = memaddr; + + (void)unused_L; + (void)unused_funcs_idx; if (!buffer || !memaddr) return NULL; - rc_init_parse_state(&parse, buffer, L, funcs_ndx); + /* first pass : determine how many memrefs are needed */ + rc_init_preparse_state(&preparse); + trigger = RC_ALLOC(rc_trigger_with_memrefs_t, &preparse.parse); + rc_parse_trigger_internal(&trigger->trigger, &preparse_memaddr, &preparse.parse); - self = RC_ALLOC(rc_trigger_t, &parse); - rc_init_parse_state_memrefs(&parse, &self->memrefs); + /* allocate the trigger and memrefs */ + rc_reset_parse_state(&preparse.parse, buffer); + trigger = RC_ALLOC(rc_trigger_with_memrefs_t, &preparse.parse); + rc_preparse_alloc_memrefs(&trigger->memrefs, &preparse); - rc_parse_trigger_internal(self, &memaddr, &parse); + /* parse the trigger */ + rc_parse_trigger_internal(&trigger->trigger, &memaddr, &preparse.parse); + trigger->trigger.has_memrefs = 1; - rc_destroy_parse_state(&parse); - return (parse.offset >= 0) ? self : NULL; + rc_destroy_preparse_state(&preparse); + return (preparse.parse.offset >= 0) ? &trigger->trigger : NULL; } int rc_trigger_state_active(int state) @@ -124,13 +133,33 @@ static void rc_reset_trigger_hitcounts(rc_trigger_t* self) { } } -int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* L) { +static void rc_update_trigger_memrefs(rc_trigger_t* self, rc_peek_t peek, void* ud) { + if (self->has_memrefs) { + rc_trigger_with_memrefs_t* trigger = (rc_trigger_with_memrefs_t*)self; + rc_update_memref_values(&trigger->memrefs, peek, ud); + } +} + +rc_memrefs_t* rc_trigger_get_memrefs(rc_trigger_t* self) { + if (self->has_memrefs) { + rc_trigger_with_memrefs_t* trigger = (rc_trigger_with_memrefs_t*)self; + return &trigger->memrefs; + } + + return NULL; +} + +int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, void* unused_L) { rc_eval_state_t eval_state; rc_condset_t* condset; + rc_typed_value_t measured_value; + int measured_from_hits = 0; int ret; char is_paused; char is_primed; + (void)unused_L; + switch (self->state) { case RC_TRIGGER_STATE_TRIGGERED: @@ -143,7 +172,7 @@ int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* case RC_TRIGGER_STATE_INACTIVE: /* not yet active. update the memrefs so deltas are correct when it becomes active, then return INACTIVE */ - rc_update_memref_values(self->memrefs, peek, ud); + rc_update_trigger_memrefs(self, peek, ud); return RC_TRIGGER_STATE_INACTIVE; default: @@ -151,18 +180,24 @@ int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* } /* update the memory references */ - rc_update_memref_values(self->memrefs, peek, ud); + rc_update_trigger_memrefs(self, peek, ud); /* process the trigger */ memset(&eval_state, 0, sizeof(eval_state)); eval_state.peek = peek; eval_state.peek_userdata = ud; - eval_state.L = L; + + measured_value.type = RC_VALUE_TYPE_NONE; if (self->requirement != NULL) { ret = rc_test_condset(self->requirement, &eval_state); - is_paused = self->requirement->is_paused; - is_primed = eval_state.primed; + is_paused = eval_state.is_paused; + is_primed = eval_state.is_primed; + + if (eval_state.measured_value.type != RC_VALUE_TYPE_NONE) { + memcpy(&measured_value, &eval_state.measured_value, sizeof(measured_value)); + measured_from_hits = eval_state.measured_from_hits; + } } else { ret = 1; is_paused = 0; @@ -177,8 +212,17 @@ int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* do { sub |= rc_test_condset(condset, &eval_state); - sub_paused &= condset->is_paused; - sub_primed |= eval_state.primed; + sub_paused &= eval_state.is_paused; + sub_primed |= eval_state.is_primed; + + if (eval_state.measured_value.type != RC_VALUE_TYPE_NONE) { + /* if no previous Measured value was captured, or the new one is greater, keep the new one */ + if (measured_value.type == RC_VALUE_TYPE_NONE || + rc_typed_value_compare(&eval_state.measured_value, &measured_value, RC_OPERATOR_GT)) { + memcpy(&measured_value, &eval_state.measured_value, sizeof(measured_value)); + measured_from_hits = eval_state.measured_from_hits; + } + } condset = condset->next; } while (condset); @@ -193,15 +237,15 @@ int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* /* if paused, the measured value may not be captured, keep the old value */ if (!is_paused) { - rc_typed_value_convert(&eval_state.measured_value, RC_VALUE_TYPE_UNSIGNED); - self->measured_value = eval_state.measured_value.value.u32; + rc_typed_value_convert(&measured_value, RC_VALUE_TYPE_UNSIGNED); + self->measured_value = measured_value.value.u32; } /* 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 * rc_reset_trigger_hitcounts in case we need to call rc_condset_is_measured_from_hitcount */ - if (eval_state.measured_from_hits) { + if (measured_from_hits) { self->measured_value = 0; } else if (is_paused && self->measured_value) { @@ -272,11 +316,11 @@ int rc_evaluate_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* return self->state; } -int rc_test_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, lua_State* L) { +int rc_test_trigger(rc_trigger_t* self, rc_peek_t peek, void* ud, void* unused_L) { /* for backwards compatibilty, rc_test_trigger always assumes the achievement is active */ self->state = RC_TRIGGER_STATE_ACTIVE; - return (rc_evaluate_trigger(self, peek, ud, L) == RC_TRIGGER_STATE_TRIGGERED); + return (rc_evaluate_trigger(self, peek, ud, unused_L) == RC_TRIGGER_STATE_TRIGGERED); } void rc_reset_trigger(rc_trigger_t* self) { diff --git a/deps/rcheevos/src/rcheevos/value.c b/deps/rcheevos/src/rcheevos/value.c index 25f2204e39..7987e14d6f 100644 --- a/deps/rcheevos/src/rcheevos/value.c +++ b/deps/rcheevos/src/rcheevos/value.c @@ -5,8 +5,6 @@ #include /* FLT_EPSILON */ #include /* fmod */ - - int rc_is_valid_variable_character(char ch, int is_first) { if (is_first) { if (!isalpha((unsigned char)ch)) @@ -27,7 +25,7 @@ static void rc_parse_cond_value(rc_value_t* self, const char** memaddr, rc_parse do { parse->measured_target = 0; /* passing is_value=1 should prevent any conflicts, but clear it out anyway */ - *next_clause = rc_parse_condset(memaddr, parse, 1); + *next_clause = rc_parse_condset(memaddr, parse); if (parse->offset < 0) { return; } @@ -52,182 +50,249 @@ static void rc_parse_cond_value(rc_value_t* self, const char** memaddr, rc_parse (*next_clause)->next = 0; } -void rc_parse_legacy_value(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse) { +static void rc_parse_legacy_value(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse) { + rc_condset_with_trailing_conditions_t* condset_with_conditions; rc_condition_t** next; rc_condset_t** next_clause; + rc_condset_t* condset; + rc_condition_t local_cond; rc_condition_t* cond; + uint32_t num_measured_conditions; char buffer[64] = "A:"; const char* buffer_ptr; char* ptr; + char c; /* convert legacy format into condset */ - self->conditions = RC_ALLOC(rc_condset_t, parse); - memset(self->conditions, 0, sizeof(rc_condset_t)); + next_clause = &self->conditions; + do { + num_measured_conditions = 0; - next = &self->conditions->conditions; - next_clause = &self->conditions->next; - - for (;; ++(*memaddr)) { - buffer[0] = 'A'; /* reset to AddSource */ - ptr = &buffer[2]; - - /* extract the next clause */ - for (;; ++(*memaddr)) { - switch (**memaddr) { - case '_': /* add next */ - case '$': /* maximum of */ - case '\0': /* end of string */ - case ':': /* end of leaderboard clause */ - case ')': /* end of rich presence macro */ - *ptr = '\0'; - break; - - case '*': - *ptr++ = '*'; - - buffer_ptr = *memaddr + 1; - if (*buffer_ptr == '-') { - buffer[0] = 'B'; /* change to SubSource */ - ++(*memaddr); /* don't copy sign */ - ++buffer_ptr; /* ignore sign when doing floating point check */ - } - else if (*buffer_ptr == '+') { - ++buffer_ptr; /* ignore sign when doing floating point check */ - } - - /* if it looks like a floating point number, add the 'f' prefix */ - while (isdigit((unsigned char)*buffer_ptr)) - ++buffer_ptr; - if (*buffer_ptr == '.') - *ptr++ = 'f'; - continue; - - default: - *ptr++ = **memaddr; - continue; + /* count the number of joiners and add one to determine the number of clauses. */ + num_measured_conditions = 1; + buffer_ptr = *memaddr; + while ((c = *buffer_ptr++) && c != '$') { + if (c == '_') { + ++num_measured_conditions; + buffer[0] = 'A'; /* reset to AddSource */ + } + else if (c == '*' && *buffer_ptr == '-') { + /* multiplication by a negative number will convert to SubSource */ + ++buffer_ptr; + buffer[0] = 'B'; } - - break; } - /* process the clause */ - buffer_ptr = buffer; - cond = rc_parse_condition(&buffer_ptr, parse, 0); + /* if last condition is SubSource, we'll need to add a dummy condition for the Measured */ + if (buffer[0] == 'B') + ++num_measured_conditions; + + condset_with_conditions = RC_ALLOC_WITH_TRAILING(rc_condset_with_trailing_conditions_t, + rc_condition_t, conditions, num_measured_conditions, parse); if (parse->offset < 0) return; - if (*buffer_ptr) { - /* whatever we copied as a single condition was not fully consumed */ - parse->offset = RC_INVALID_COMPARISON; - return; - } + condset = (rc_condset_t*)condset_with_conditions; + memset(condset, 0, sizeof(*condset)); + condset->num_measured_conditions = num_measured_conditions; + cond = &condset_with_conditions->conditions[0]; + + next = &condset->conditions; + + for (;; ++(*memaddr)) { + buffer[0] = 'A'; /* reset to AddSource */ + ptr = &buffer[2]; + + /* extract the next clause */ + for (;; ++(*memaddr)) { + switch (**memaddr) { + case '_': /* add next */ + case '$': /* maximum of */ + case '\0': /* end of string */ + case ':': /* end of leaderboard clause */ + case ')': /* end of rich presence macro */ + *ptr = '\0'; + break; + + case '*': + *ptr++ = '*'; + + buffer_ptr = *memaddr + 1; + if (*buffer_ptr == '-') { + buffer[0] = 'B'; /* change to SubSource */ + ++(*memaddr); /* don't copy sign */ + ++buffer_ptr; /* ignore sign when doing floating point check */ + } + else if (*buffer_ptr == '+') { + ++buffer_ptr; /* ignore sign when doing floating point check */ + } + + /* if it looks like a floating point number, add the 'f' prefix */ + while (isdigit((unsigned char)*buffer_ptr)) + ++buffer_ptr; + if (*buffer_ptr == '.') + *ptr++ = 'f'; + continue; + + default: + *ptr++ = **memaddr; + continue; + } - switch (cond->oper) { - case RC_OPERATOR_MULT: - case RC_OPERATOR_DIV: - case RC_OPERATOR_AND: - case RC_OPERATOR_XOR: - case RC_OPERATOR_MOD: - case RC_OPERATOR_ADD: - case RC_OPERATOR_SUB: - case RC_OPERATOR_NONE: break; + } - default: + /* process the clause */ + if (!parse->buffer) + cond = &local_cond; + + buffer_ptr = buffer; + rc_parse_condition_internal(cond, &buffer_ptr, parse); + if (parse->offset < 0) + return; + + if (*buffer_ptr) { + /* whatever we copied as a single condition was not fully consumed */ + parse->offset = RC_INVALID_COMPARISON; + return; + } + + if (!rc_operator_is_modifying(cond->oper)) { parse->offset = RC_INVALID_OPERATOR; return; - } + } - *next = cond; - - if (**memaddr == '_') { - /* add next */ + *next = cond; next = &cond->next; - continue; + + if (**memaddr != '_') /* add next */ + break; + + rc_condition_update_parse_state(cond, parse); + ++cond; } + /* end of clause */ if (cond->type == RC_CONDITION_SUB_SOURCE) { /* cannot change SubSource to Measured. add a dummy condition */ - next = &cond->next; + rc_condition_update_parse_state(cond, parse); + if (parse->buffer) + ++cond; + buffer_ptr = "A:0"; - cond = rc_parse_condition(&buffer_ptr, parse, 0); + rc_parse_condition_internal(cond, &buffer_ptr, parse); *next = cond; + next = &cond->next; } /* convert final AddSource condition to Measured */ cond->type = RC_CONDITION_MEASURED; - cond->next = 0; + cond->next = NULL; + rc_condition_update_parse_state(cond, parse); + + /* finalize clause */ + *next_clause = condset; + next_clause = &condset->next; if (**memaddr != '$') { /* end of valid string */ - *next_clause = 0; + *next_clause = NULL; break; } /* max of ($), start a new clause */ - *next_clause = RC_ALLOC(rc_condset_t, parse); - - if (parse->buffer) /* don't clear in sizing mode or pointer will break */ - memset(*next_clause, 0, sizeof(rc_condset_t)); - - next = &(*next_clause)->conditions; - next_clause = &(*next_clause)->next; - } + ++(*memaddr); + } while (1); } void rc_parse_value_internal(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse) { + const uint8_t was_value = parse->is_value; + const rc_condition_t* condition; + parse->is_value = 1; + /* if it starts with a condition flag (M: A: B: C:), parse the conditions */ - if ((*memaddr)[1] == ':') { + if ((*memaddr)[1] == ':') rc_parse_cond_value(self, memaddr, parse); - } - else { + else rc_parse_legacy_value(self, memaddr, parse); + + if (parse->offset >= 0 && parse->buffer) { + self->name = "(unnamed)"; + self->value.value = self->value.prior = 0; + self->value.memref_type = RC_MEMREF_TYPE_VALUE; + self->value.changed = 0; + self->has_memrefs = 0; + + for (condition = self->conditions->conditions; condition; condition = condition->next) { + if (condition->type == RC_CONDITION_MEASURED) { + if (rc_operand_is_float(&condition->operand1)) { + self->value.size = RC_MEMSIZE_FLOAT; + self->value.type = RC_VALUE_TYPE_FLOAT; + } + else { + self->value.size = RC_MEMSIZE_32_BITS; + self->value.type = RC_VALUE_TYPE_UNSIGNED; + } + break; + } + } } - self->name = "(unnamed)"; - self->value.value = self->value.prior = 0; - self->value.changed = 0; - self->next = 0; + parse->is_value = was_value; } int rc_value_size(const char* memaddr) { - rc_value_t* self; - rc_parse_state_t parse; - rc_memref_t* first_memref; - rc_init_parse_state(&parse, 0, 0, 0); - rc_init_parse_state_memrefs(&parse, &first_memref); + rc_value_with_memrefs_t* value; + rc_preparse_state_t preparse; + rc_init_preparse_state(&preparse); - self = RC_ALLOC(rc_value_t, &parse); - rc_parse_value_internal(self, &memaddr, &parse); + value = RC_ALLOC(rc_value_with_memrefs_t, &preparse.parse); + rc_parse_value_internal(&value->value, &memaddr, &preparse.parse); + rc_preparse_alloc_memrefs(NULL, &preparse); - rc_destroy_parse_state(&parse); - return parse.offset; + rc_destroy_preparse_state(&preparse); + return preparse.parse.offset; } -rc_value_t* rc_parse_value(void* buffer, const char* memaddr, lua_State* L, int funcs_ndx) { - rc_value_t* self; - rc_parse_state_t parse; +rc_value_t* rc_parse_value(void* buffer, const char* memaddr, void* unused_L, int unused_funcs_idx) { + rc_value_with_memrefs_t* value; + rc_preparse_state_t preparse; + const char* preparse_memaddr = memaddr; + + (void)unused_L; + (void)unused_funcs_idx; if (!buffer || !memaddr) return NULL; - rc_init_parse_state(&parse, buffer, L, funcs_ndx); + rc_init_preparse_state(&preparse); + value = RC_ALLOC(rc_value_with_memrefs_t, &preparse.parse); + rc_parse_value_internal(&value->value, &preparse_memaddr, &preparse.parse); - self = RC_ALLOC(rc_value_t, &parse); - rc_init_parse_state_memrefs(&parse, &self->memrefs); + rc_reset_parse_state(&preparse.parse, buffer); + value = RC_ALLOC(rc_value_with_memrefs_t, &preparse.parse); + rc_preparse_alloc_memrefs(&value->memrefs, &preparse); - rc_parse_value_internal(self, &memaddr, &parse); + rc_parse_value_internal(&value->value, &memaddr, &preparse.parse); + value->value.has_memrefs = 1; - rc_destroy_parse_state(&parse); - return (parse.offset >= 0) ? self : NULL; + rc_destroy_preparse_state(&preparse); + return (preparse.parse.offset >= 0) ? &value->value : NULL; } -int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t peek, void* ud, lua_State* L) { +static void rc_update_value_memrefs(rc_value_t* self, rc_peek_t peek, void* ud) { + if (self->has_memrefs) { + rc_value_with_memrefs_t* value = (rc_value_with_memrefs_t*)self; + rc_update_memref_values(&value->memrefs, peek, ud); + } +} + +int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t peek, void* ud) { rc_eval_state_t eval_state; rc_condset_t* condset; int valid = 0; - rc_update_memref_values(self->memrefs, peek, ud); + rc_update_value_memrefs(self, peek, ud); value->value.i32 = 0; value->type = RC_VALUE_TYPE_SIGNED; @@ -236,7 +301,6 @@ int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t memset(&eval_state, 0, sizeof(eval_state)); eval_state.peek = peek; eval_state.peek_userdata = ud; - eval_state.L = L; rc_test_condset(condset, &eval_state); @@ -248,34 +312,32 @@ int rc_evaluate_value_typed(rc_value_t* self, rc_typed_value_t* value, rc_peek_t * NOTE: ResetIf only affects the current condset when used in values! */ rc_reset_condset(condset); - - /* if the measured value came from a hit count, reset it too */ - if (eval_state.measured_from_hits) { - eval_state.measured_value.value.u32 = 0; - eval_state.measured_value.type = RC_VALUE_TYPE_UNSIGNED; - } } - if (!valid) { - /* capture the first valid measurement */ - memcpy(value, &eval_state.measured_value, sizeof(*value)); - valid = 1; - } - else { - /* multiple condsets are currently only used for the MAX_OF operation. - * only keep the condset's value if it's higher than the current highest value. - */ - if (rc_typed_value_compare(&eval_state.measured_value, value, RC_OPERATOR_GT)) + if (eval_state.measured_value.type != RC_VALUE_TYPE_NONE) { + if (!valid) { + /* capture the first valid measurement, which may be negative */ memcpy(value, &eval_state.measured_value, sizeof(*value)); + valid = 1; + } + else { + /* multiple condsets are currently only used for the MAX_OF operation. + * only keep the condset's value if it's higher than the current highest value. + */ + if (rc_typed_value_compare(&eval_state.measured_value, value, RC_OPERATOR_GT)) + memcpy(value, &eval_state.measured_value, sizeof(*value)); + } } } return valid; } -int32_t rc_evaluate_value(rc_value_t* self, rc_peek_t peek, void* ud, lua_State* L) { +int32_t rc_evaluate_value(rc_value_t* self, rc_peek_t peek, void* ud, void* unused_L) { rc_typed_value_t result; - int valid = rc_evaluate_value_typed(self, &result, peek, ud, L); + int valid = rc_evaluate_value_typed(self, &result, peek, ud); + + (void)unused_L; if (valid) { /* if not paused, store the value so that it's available when paused. */ @@ -317,84 +379,81 @@ int rc_value_from_hits(rc_value_t* self) return 0; } -void rc_init_parse_state_variables(rc_parse_state_t* parse, rc_value_t** variables) { - parse->variables = variables; - *variables = 0; -} - -rc_value_t* rc_alloc_helper_variable(const char* memaddr, size_t memaddr_len, rc_parse_state_t* parse) -{ - rc_value_t** variables = parse->variables; +rc_value_t* rc_alloc_variable(const char* memaddr, size_t memaddr_len, rc_parse_state_t* parse) { + rc_value_t** value_ptr = parse->variables; rc_value_t* value; const char* name; uint32_t measured_target; - while ((value = *variables) != NULL) { + if (!value_ptr) + return NULL; + + while (*value_ptr) { + value = *value_ptr; if (strncmp(value->name, memaddr, memaddr_len) == 0 && value->name[memaddr_len] == 0) return value; - variables = &value->next; + value_ptr = &value->next; } - value = RC_ALLOC_SCRATCH(rc_value_t, parse); - memset(&value->value, 0, sizeof(value->value)); - value->value.size = RC_MEMSIZE_VARIABLE; - value->memrefs = NULL; - /* capture name before calling parse as parse will update memaddr pointer */ name = rc_alloc_str(parse, memaddr, memaddr_len); if (!name) return NULL; + /* no match found, create a new entry */ + value = RC_ALLOC_SCRATCH(rc_value_t, parse); + memset(value, 0, sizeof(value->value)); + value->value.size = RC_MEMSIZE_VARIABLE; + value->next = NULL; + /* the helper variable likely has a Measured condition. capture the current measured_target so we can restore it * after generating the variable so the variable's Measured target doesn't conflict with the rest of the trigger. */ measured_target = parse->measured_target; - - /* disable variable resolution when defining a variable to prevent infinite recursion */ - variables = parse->variables; - parse->variables = NULL; rc_parse_value_internal(value, &memaddr, parse); - parse->variables = variables; - - /* restore the measured target */ parse->measured_target = measured_target; /* store name after calling parse as parse will set name to (unnamed) */ value->name = name; - /* append the new variable to the end of the list (have to re-evaluate in case any others were added) */ - while (*variables != NULL) - variables = &(*variables)->next; - *variables = value; - + *value_ptr = value; return value; } -void rc_update_variables(rc_value_t* variable, rc_peek_t peek, void* ud, lua_State* L) { +uint32_t rc_count_values(const rc_value_t* values) { + uint32_t count = 0; + while (values) { + ++count; + values = values->next; + } + + return count; +} + +void rc_update_values(rc_value_t* values, rc_peek_t peek, void* ud) { rc_typed_value_t result; - while (variable) { - if (rc_evaluate_value_typed(variable, &result, peek, ud, L)) { + rc_value_t* value = values; + for (; value; value = value->next) { + if (rc_evaluate_value_typed(value, &result, peek, ud)) { /* store the raw bytes and type to be restored by rc_typed_value_from_memref_value */ - rc_update_memref_value(&variable->value, result.value.u32); - variable->value.type = result.type; + rc_update_memref_value(&value->value, result.value.u32); + value->value.type = result.type; } - - variable = variable->next; } } -void rc_typed_value_from_memref_value(rc_typed_value_t* value, const rc_memref_value_t* memref) { - value->value.u32 = memref->value; +void rc_reset_values(rc_value_t* values) { + rc_value_t* value = values; - if (memref->size == RC_MEMSIZE_VARIABLE) { - /* a variable can be any of the supported types, but the raw data was copied into u32 */ - value->type = memref->type; - } - else { - /* not a variable, only u32 is supported */ - value->type = RC_VALUE_TYPE_UNSIGNED; - } + for (; value; value = value->next) + rc_reset_value(value); +} + +void rc_typed_value_from_memref_value(rc_typed_value_t* value, const rc_memref_value_t* memref) { + /* raw value is always u32, type can mark it as something else */ + value->value.u32 = memref->value; + value->type = memref->type; } void rc_typed_value_convert(rc_typed_value_t* value, char new_type) { @@ -483,8 +542,12 @@ void rc_typed_value_negate(rc_typed_value_t* value) { void rc_typed_value_add(rc_typed_value_t* value, const rc_typed_value_t* amount) { rc_typed_value_t converted; - if (amount->type != value->type && value->type != RC_VALUE_TYPE_NONE) - amount = rc_typed_value_convert_into(&converted, amount, value->type); + if (amount->type != value->type && value->type != RC_VALUE_TYPE_NONE) { + if (amount->type == RC_VALUE_TYPE_FLOAT) + rc_typed_value_convert(value, RC_VALUE_TYPE_FLOAT); + else + amount = rc_typed_value_convert_into(&converted, amount, value->type); + } switch (value->type) { @@ -651,56 +714,56 @@ void rc_typed_value_modulus(rc_typed_value_t* value, const rc_typed_value_t* amo switch (amount->type) { - case RC_VALUE_TYPE_UNSIGNED: - if (amount->value.u32 == 0) { /* divide by zero */ - value->type = RC_VALUE_TYPE_NONE; - return; - } + case RC_VALUE_TYPE_UNSIGNED: + if (amount->value.u32 == 0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } - switch (value->type) { - case RC_VALUE_TYPE_UNSIGNED: /* integer math */ - value->value.u32 %= amount->value.u32; - return; - case RC_VALUE_TYPE_SIGNED: /* integer math */ - value->value.i32 %= (int)amount->value.u32; - return; - case RC_VALUE_TYPE_FLOAT: - amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + switch (value->type) { + case RC_VALUE_TYPE_UNSIGNED: /* integer math */ + value->value.u32 %= amount->value.u32; + return; + case RC_VALUE_TYPE_SIGNED: /* integer math */ + value->value.i32 %= (int)amount->value.u32; + return; + case RC_VALUE_TYPE_FLOAT: + amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + break; + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } break; + + case RC_VALUE_TYPE_SIGNED: + if (amount->value.i32 == 0) { /* divide by zero */ + value->type = RC_VALUE_TYPE_NONE; + return; + } + + switch (value->type) { + case RC_VALUE_TYPE_SIGNED: /* integer math */ + value->value.i32 %= amount->value.i32; + return; + case RC_VALUE_TYPE_UNSIGNED: /* integer math */ + value->value.u32 %= (unsigned)amount->value.i32; + return; + case RC_VALUE_TYPE_FLOAT: + amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); + break; + default: + value->type = RC_VALUE_TYPE_NONE; + return; + } + break; + + case RC_VALUE_TYPE_FLOAT: + break; + default: value->type = RC_VALUE_TYPE_NONE; return; - } - break; - - case RC_VALUE_TYPE_SIGNED: - if (amount->value.i32 == 0) { /* divide by zero */ - value->type = RC_VALUE_TYPE_NONE; - return; - } - - switch (value->type) { - case RC_VALUE_TYPE_SIGNED: /* integer math */ - value->value.i32 %= amount->value.i32; - return; - case RC_VALUE_TYPE_UNSIGNED: /* integer math */ - value->value.u32 %= (unsigned)amount->value.i32; - return; - case RC_VALUE_TYPE_FLOAT: - amount = rc_typed_value_convert_into(&converted, amount, RC_VALUE_TYPE_FLOAT); - break; - default: - value->type = RC_VALUE_TYPE_NONE; - return; - } - break; - - case RC_VALUE_TYPE_FLOAT: - break; - - default: - value->type = RC_VALUE_TYPE_NONE; - return; } if (amount->value.f32 == 0.0) { /* divide by zero */ @@ -712,6 +775,44 @@ void rc_typed_value_modulus(rc_typed_value_t* value, const rc_typed_value_t* amo value->value.f32 = (float)fmod(value->value.f32, amount->value.f32); } +void rc_typed_value_combine(rc_typed_value_t* value, rc_typed_value_t* amount, uint8_t oper) { + switch (oper) { + case RC_OPERATOR_MULT: + rc_typed_value_multiply(value, amount); + break; + + case RC_OPERATOR_DIV: + rc_typed_value_divide(value, amount); + break; + + case RC_OPERATOR_AND: + 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; + + 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; + + case RC_OPERATOR_MOD: + rc_typed_value_modulus(value, amount); + break; + + case RC_OPERATOR_ADD: + rc_typed_value_add(value, amount); + break; + + case RC_OPERATOR_SUB: + rc_typed_value_negate(amount); + rc_typed_value_add(value, amount); + break; + } +} + + static int rc_typed_value_compare_floats(float f1, float f2, char oper) { if (f1 == f2) { /* exactly equal */ diff --git a/deps/rcheevos/src/rhash/cdreader.c b/deps/rcheevos/src/rhash/cdreader.c index 9ff9cb7396..9ffcc55bcb 100644 --- a/deps/rcheevos/src/rhash/cdreader.c +++ b/deps/rcheevos/src/rhash/cdreader.c @@ -1,4 +1,4 @@ -#include "rc_hash.h" +#include "rc_hash_internal.h" #include "../rc_compat.h" @@ -6,31 +6,6 @@ #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_hash_error(const char* message); -extern const char* rc_path_get_filename(const char* path); -extern int rc_path_compare_extension(const char* path, const char* ext); -extern rc_hash_message_callback verbose_message_callback; - -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 */ -#ifndef NDEBUG - uint32_t track_id; /* the index of the track */ -#endif -}; - static int cdreader_get_sector(uint8_t header[16]) { int minutes = (header[12] >> 4) * 10 + (header[12] & 0x0F); @@ -43,7 +18,7 @@ static int cdreader_get_sector(uint8_t header[16]) return ((minutes * 60) + seconds) * 75 + frames - 150; } -static void cdreader_determine_sector_size(struct cdrom_t* cdrom) +static void cdreader_determine_sector_size(rc_hash_cdrom_track_t* cdrom) { /* Attempt to determine the sector and header sizes. The CUE file may be lying. * Look for the sync pattern using each of the supported sector sizes. @@ -61,8 +36,8 @@ static void cdreader_determine_sector_size(struct cdrom_t* cdrom) 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)) + cdrom->file_reader->seek(cdrom->file_handle, toc_sector * 2352 + cdrom->file_track_offset, SEEK_SET); + if (cdrom->file_reader->read(cdrom->file_handle, header, sizeof(header)) < sizeof(header)) return; if (memcmp(header, sync_pattern, 12) == 0) @@ -78,8 +53,8 @@ static void cdreader_determine_sector_size(struct cdrom_t* cdrom) } else { - rc_file_seek(cdrom->file_handle, toc_sector * 2336 + cdrom->file_track_offset, SEEK_SET); - rc_file_read(cdrom->file_handle, header, sizeof(header)); + cdrom->file_reader->seek(cdrom->file_handle, toc_sector * 2336 + cdrom->file_track_offset, SEEK_SET); + cdrom->file_reader->read(cdrom->file_handle, header, sizeof(header)); if (memcmp(header, sync_pattern, 12) == 0) { @@ -94,8 +69,8 @@ static void cdreader_determine_sector_size(struct cdrom_t* cdrom) } else { - rc_file_seek(cdrom->file_handle, toc_sector * 2048 + cdrom->file_track_offset, SEEK_SET); - rc_file_read(cdrom->file_handle, header, sizeof(header)); + cdrom->file_reader->seek(cdrom->file_handle, toc_sector * 2048 + cdrom->file_track_offset, SEEK_SET); + cdrom->file_reader->read(cdrom->file_handle, header, sizeof(header)); if (memcmp(&header[1], "CD001", 5) == 0) { @@ -106,26 +81,24 @@ static void cdreader_determine_sector_size(struct cdrom_t* cdrom) } } -static void* cdreader_open_bin_track(const char* path, uint32_t track) +static void* cdreader_open_bin_track(const char* path, uint32_t track, const rc_hash_iterator_t* iterator) { void* file_handle; - struct cdrom_t* cdrom; - - if (track > 1) - { - if (verbose_message_callback) - verbose_message_callback("Cannot locate secondary tracks without a cue sheet"); + rc_hash_cdrom_track_t* cdrom; + if (track > 1) { + rc_hash_iterator_verbose(iterator, "Cannot locate secondary tracks without a cue sheet"); return NULL; } - file_handle = rc_file_open(path); + file_handle = iterator->callbacks.filereader.open(path); if (!file_handle) return NULL; - cdrom = (struct cdrom_t*)calloc(1, sizeof(*cdrom)); + cdrom = (rc_hash_cdrom_track_t*)calloc(1, sizeof(*cdrom)); if (!cdrom) return NULL; + cdrom->file_reader = &iterator->callbacks.filereader; cdrom->file_handle = file_handle; #ifndef NDEBUG cdrom->track_id = track; @@ -137,8 +110,8 @@ static void* cdreader_open_bin_track(const char* path, uint32_t track) { int64_t size; - rc_file_seek(cdrom->file_handle, 0, SEEK_END); - size = rc_file_tell(cdrom->file_handle); + iterator->callbacks.filereader.seek(file_handle, 0, SEEK_END); + size = iterator->callbacks.filereader.tell(file_handle); if ((size % 2352) == 0) { @@ -160,10 +133,11 @@ static void* cdreader_open_bin_track(const char* path, uint32_t track) } else { + if (iterator->callbacks.filereader.close) + iterator->callbacks.filereader.close(file_handle); free(cdrom); - if (verbose_message_callback) - verbose_message_callback("Could not determine sector size"); + rc_hash_iterator_verbose(iterator, "Could not determine sector size"); return NULL; } @@ -172,9 +146,9 @@ static void* cdreader_open_bin_track(const char* path, uint32_t track) return cdrom; } -static int cdreader_open_bin(struct cdrom_t* cdrom, const char* path, const char* mode) +static int cdreader_open_bin(rc_hash_cdrom_track_t* cdrom, const char* path, const char* mode) { - cdrom->file_handle = rc_file_open(path); + cdrom->file_handle = cdrom->file_reader->open(path); if (!cdrom->file_handle) return 0; @@ -230,7 +204,7 @@ static int cdreader_open_bin(struct cdrom_t* cdrom, const char* path, const char return (cdrom->sector_size != 0); } -static char* cdreader_get_bin_path(const char* cue_path, const char* bin_name) +static char* cdreader_get_bin_path(const char* cue_path, const char* bin_name, const rc_hash_iterator_t* iterator) { const char* filename = rc_path_get_filename(cue_path); const size_t bin_name_len = strlen(bin_name); @@ -240,9 +214,7 @@ static char* cdreader_get_bin_path(const char* cue_path, const char* bin_name) char* bin_filename = (char*)malloc(needed); if (!bin_filename) { - char buffer[64]; - snprintf(buffer, sizeof(buffer), "Failed to allocate %u bytes", (unsigned)needed); - rc_hash_error((const char*)buffer); + rc_hash_iterator_error_formatted(iterator, "Failed to allocate %u bytes", (unsigned)needed); } else { @@ -253,33 +225,29 @@ static char* cdreader_get_bin_path(const char* cue_path, const char* bin_name) return bin_filename; } -static int64_t cdreader_get_bin_size(const char* cue_path, const char* bin_name) +static int64_t cdreader_get_bin_size(const char* cue_path, const char* bin_name, const rc_hash_iterator_t* iterator) { int64_t size = 0; - char* bin_filename = cdreader_get_bin_path(cue_path, bin_name); + char* bin_filename = cdreader_get_bin_path(cue_path, bin_name, iterator); if (bin_filename) { - /* disable verbose messaging while getting file size */ - rc_hash_message_callback old_verbose_message_callback = verbose_message_callback; - void* file_handle; - verbose_message_callback = NULL; - - file_handle = rc_file_open(bin_filename); - if (file_handle) + void* handle = iterator->callbacks.filereader.open(bin_filename); + if (handle) { - rc_file_seek(file_handle, 0, SEEK_END); - size = rc_file_tell(file_handle); - rc_file_close(file_handle); + iterator->callbacks.filereader.seek(handle, 0, SEEK_END); + size = iterator->callbacks.filereader.tell(handle); + + if (iterator->callbacks.filereader.close) + iterator->callbacks.filereader.close(handle); } - verbose_message_callback = old_verbose_message_callback; free(bin_filename); } return size; } -static void* cdreader_open_cue_track(const char* path, uint32_t track) +static void* cdreader_open_cue_track(const char* path, uint32_t track, const rc_hash_iterator_t* iterator) { void* cue_handle; int64_t cue_offset = 0; @@ -289,7 +257,7 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) int done = 0; int session = 1; size_t num_read = 0; - struct cdrom_t* cdrom = NULL; + rc_hash_cdrom_track_t* cdrom = NULL; struct track_t { @@ -305,7 +273,7 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) char filename[256]; } current_track, previous_track, largest_track; - cue_handle = rc_file_open(path); + cue_handle = iterator->callbacks.filereader.open(path); if (!cue_handle) return NULL; @@ -315,7 +283,7 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) do { - num_read = rc_file_read(cue_handle, buffer, sizeof(buffer) - 1); + num_read = iterator->callbacks.filereader.read(cue_handle, buffer, sizeof(buffer) - 1); if (num_read == 0) break; @@ -369,18 +337,15 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) { current_track.pregap_sectors = (sector_offset - current_track.first_sector); - if (verbose_message_callback) { - char message[128]; char* scan = current_track.mode; while (*scan && !isspace((unsigned char)*scan)) ++scan; *scan = '\0'; /* it's undesirable to truncate offset to 32-bits, but %lld isn't defined in c89. */ - snprintf(message, sizeof(message), "Found %s track %d (first sector %d, sector size %d, %d pregap sectors)", + rc_hash_iterator_verbose_formatted(iterator, "Found %s track %d (first sector %d, sector size %d, %d pregap sectors)", current_track.mode, current_track.id, current_track.first_sector, current_track.sector_size, current_track.pregap_sectors); - verbose_message_callback(message); } if (current_track.id == track) @@ -440,7 +405,8 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) if (previous_track.sector_count == 0) { - const uint32_t file_sector_count = (uint32_t)cdreader_get_bin_size(path, previous_track.filename) / previous_track.sector_size; + const int64_t bin_size = cdreader_get_bin_size(path, previous_track.filename, iterator); + const uint32_t file_sector_count = (uint32_t)bin_size / previous_track.sector_size; previous_track.sector_count = file_sector_count - previous_track.first_sector; } @@ -501,17 +467,19 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) break; cue_offset += (ptr - buffer); - rc_file_seek(cue_handle, cue_offset, SEEK_SET); + iterator->callbacks.filereader.seek(cue_handle, cue_offset, SEEK_SET); } while (1); - rc_file_close(cue_handle); + if (iterator->callbacks.filereader.close) + iterator->callbacks.filereader.close(cue_handle); if (track == RC_HASH_CDTRACK_LARGEST) { if (current_track.sector_size && current_track.is_data) { - const uint32_t file_sector_count = (uint32_t)cdreader_get_bin_size(path, current_track.filename) / current_track.sector_size; + const int64_t bin_size = cdreader_get_bin_size(path, current_track.filename, iterator); + const uint32_t file_sector_count = (uint32_t)bin_size / current_track.sector_size; current_track.sector_count = file_sector_count - current_track.first_sector; if (largest_track.sector_count > current_track.sector_count) @@ -531,14 +499,14 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) if (current_track.id == track) { - cdrom = (struct cdrom_t*)calloc(1, sizeof(*cdrom)); + cdrom = (rc_hash_cdrom_track_t*)calloc(1, sizeof(*cdrom)); if (!cdrom) { - snprintf((char*)buffer, sizeof(buffer), "Failed to allocate %u bytes", (unsigned)sizeof(*cdrom)); - rc_hash_error((const char*)buffer); + rc_hash_iterator_error_formatted(iterator, "Failed to allocate %u bytes", (unsigned)sizeof(*cdrom)); return NULL; } + cdrom->file_reader = &iterator->callbacks.filereader; cdrom->file_track_offset = current_track.file_track_offset; cdrom->track_pregap_sectors = current_track.pregap_sectors; cdrom->track_first_sector = current_track.file_first_sector + current_track.first_sector; @@ -547,36 +515,29 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) #endif /* verify existance of bin file */ - bin_filename = cdreader_get_bin_path(path, current_track.filename); + bin_filename = cdreader_get_bin_path(path, current_track.filename, iterator); if (bin_filename) { if (cdreader_open_bin(cdrom, bin_filename, current_track.mode)) { - if (verbose_message_callback) - { - if (cdrom->track_pregap_sectors) - snprintf((char*)buffer, sizeof(buffer), "Opened track %d (sector size %d, %d pregap sectors)", - track, cdrom->sector_size, cdrom->track_pregap_sectors); - else - snprintf((char*)buffer, sizeof(buffer), "Opened track %d (sector size %d)", track, cdrom->sector_size); - - verbose_message_callback((const char*)buffer); - } + if (cdrom->track_pregap_sectors) + rc_hash_iterator_verbose_formatted(iterator, "Opened track %d (sector size %d, %d pregap sectors)", + track, cdrom->sector_size, cdrom->track_pregap_sectors); + else + rc_hash_iterator_verbose_formatted(iterator, "Opened track %d (sector size %d)", track, cdrom->sector_size); } else { if (cdrom->file_handle) { - rc_file_close(cdrom->file_handle); - snprintf((char*)buffer, sizeof(buffer), "Could not determine sector size for %s track", current_track.mode); + cdrom->file_reader->close(cdrom->file_handle); + rc_hash_iterator_error_formatted(iterator, "Could not determine sector size for %s track", current_track.mode); } else { - snprintf((char*)buffer, sizeof(buffer), "Could not open %s", bin_filename); + rc_hash_iterator_error_formatted(iterator, "Could not open %s", bin_filename); } - rc_hash_error((const char*)buffer); - free(cdrom); cdrom = NULL; } @@ -588,7 +549,7 @@ static void* cdreader_open_cue_track(const char* path, uint32_t track) return cdrom; } -static void* cdreader_open_gdi_track(const char* path, uint32_t track) +static void* cdreader_open_gdi_track(const char* path, uint32_t track, const rc_hash_iterator_t* iterator) { void* file_handle; char buffer[1024]; @@ -611,16 +572,16 @@ static void* cdreader_open_gdi_track(const char* path, uint32_t track) int found = 0; size_t num_read = 0; int64_t file_offset = 0; - struct cdrom_t* cdrom = NULL; + rc_hash_cdrom_track_t* cdrom = NULL; - file_handle = rc_file_open(path); + file_handle = iterator->callbacks.filereader.open(path); if (!file_handle) return NULL; file[0] = '\0'; do { - num_read = rc_file_read(file_handle, buffer, sizeof(buffer) - 1); + num_read = iterator->callbacks.filereader.read(file_handle, buffer, sizeof(buffer) - 1); if (num_read == 0) break; @@ -715,7 +676,7 @@ static void* cdreader_open_gdi_track(const char* path, uint32_t track) } else if (track == RC_HASH_CDTRACK_LARGEST && track_type == 4) { - track_size = cdreader_get_bin_size(path, file); + track_size = cdreader_get_bin_size(path, file, iterator); if (track_size > largest_track_size) { largest_track_size = track_size; @@ -731,20 +692,22 @@ static void* cdreader_open_gdi_track(const char* path, uint32_t track) break; file_offset += (ptr - buffer); - rc_file_seek(file_handle, file_offset, SEEK_SET); + iterator->callbacks.filereader.seek(file_handle, file_offset, SEEK_SET); } while (1); - rc_file_close(file_handle); + if (iterator->callbacks.filereader.close) + iterator->callbacks.filereader.close(file_handle); - cdrom = (struct cdrom_t*)calloc(1, sizeof(*cdrom)); + cdrom = (rc_hash_cdrom_track_t*)calloc(1, sizeof(*cdrom)); if (!cdrom) { - snprintf((char*)buffer, sizeof(buffer), "Failed to allocate %u bytes", (unsigned)sizeof(*cdrom)); - rc_hash_error((const char*)buffer); + rc_hash_iterator_error_formatted(iterator, "Failed to allocate %u bytes", (unsigned)sizeof(*cdrom)); return NULL; } + cdrom->file_reader = &iterator->callbacks.filereader; + /* if we were tracking the largest track, make it the current track. * otherwise, current_track will be the requested track, or last track. */ if (largest_track != 0 && largest_track != current_track) @@ -762,7 +725,7 @@ static void* cdreader_open_gdi_track(const char* path, uint32_t track) *ptr++ = *ptr2++; *ptr = '\0'; - bin_path = cdreader_get_bin_path(path, file); + bin_path = cdreader_get_bin_path(path, file, iterator); if (cdreader_open_bin(cdrom, bin_path, mode)) { cdrom->track_pregap_sectors = 0; @@ -771,16 +734,11 @@ static void* cdreader_open_gdi_track(const char* path, uint32_t track) cdrom->track_id = current_track; #endif - if (verbose_message_callback) - { - snprintf((char*)buffer, sizeof(buffer), "Opened track %d (sector size %d)", current_track, cdrom->sector_size); - verbose_message_callback((const char*)buffer); - } + rc_hash_iterator_verbose_formatted(iterator, "Opened track %d (sector size %d)", current_track, cdrom->sector_size); } else { - snprintf((char*)buffer, sizeof(buffer), "Could not open %s", bin_path); - rc_hash_error((const char*)buffer); + rc_hash_iterator_error_formatted(iterator, "Could not open %s", bin_path); free(cdrom); cdrom = NULL; @@ -791,18 +749,18 @@ static void* cdreader_open_gdi_track(const char* path, uint32_t track) return cdrom; } -static void* cdreader_open_track(const char* path, uint32_t track) +static void* cdreader_open_track_iterator(const char* path, uint32_t track, const rc_hash_iterator_t* iterator) { /* backwards compatibility - 0 used to mean largest */ if (track == 0) track = RC_HASH_CDTRACK_LARGEST; if (rc_path_compare_extension(path, "cue")) - return cdreader_open_cue_track(path, track); + return cdreader_open_cue_track(path, track, iterator); if (rc_path_compare_extension(path, "gdi")) - return cdreader_open_gdi_track(path, track); + return cdreader_open_gdi_track(path, track, iterator); - return cdreader_open_bin_track(path, track); + return cdreader_open_bin_track(path, track, iterator); } static size_t cdreader_read_sector(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) @@ -811,7 +769,7 @@ static size_t cdreader_read_sector(void* track_handle, uint32_t sector, void* bu size_t num_read, total_read = 0; uint8_t* buffer_ptr = (uint8_t*)buffer; - struct cdrom_t* cdrom = (struct cdrom_t*)track_handle; + rc_hash_cdrom_track_t* cdrom = (rc_hash_cdrom_track_t*)track_handle; if (!cdrom) return 0; @@ -823,8 +781,8 @@ static size_t cdreader_read_sector(void* track_handle, uint32_t sector, void* bu 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, cdrom->raw_data_size); + cdrom->file_reader->seek(cdrom->file_handle, sector_start, SEEK_SET); + num_read = cdrom->file_reader->read(cdrom->file_handle, buffer_ptr, cdrom->raw_data_size); total_read += num_read; if (num_read < (size_t)cdrom->raw_data_size) @@ -835,8 +793,8 @@ static size_t cdreader_read_sector(void* track_handle, uint32_t sector, void* bu requested_bytes -= cdrom->raw_data_size; } - rc_file_seek(cdrom->file_handle, sector_start, SEEK_SET); - num_read = rc_file_read(cdrom->file_handle, buffer_ptr, (int)requested_bytes); + cdrom->file_reader->seek(cdrom->file_handle, sector_start, SEEK_SET); + num_read = cdrom->file_reader->read(cdrom->file_handle, buffer_ptr, (int)requested_bytes); total_read += num_read; return total_read; @@ -844,11 +802,11 @@ static size_t cdreader_read_sector(void* track_handle, uint32_t sector, void* bu static void cdreader_close_track(void* track_handle) { - struct cdrom_t* cdrom = (struct cdrom_t*)track_handle; + rc_hash_cdrom_track_t* cdrom = (rc_hash_cdrom_track_t*)track_handle; if (cdrom) { - if (cdrom->file_handle) - rc_file_close(cdrom->file_handle); + if (cdrom->file_handle && cdrom->file_reader->close) + cdrom->file_reader->close(cdrom->file_handle); free(track_handle); } @@ -856,7 +814,7 @@ static void cdreader_close_track(void* track_handle) static uint32_t cdreader_first_track_sector(void* track_handle) { - struct cdrom_t* cdrom = (struct cdrom_t*)track_handle; + rc_hash_cdrom_track_t* cdrom = (rc_hash_cdrom_track_t*)track_handle; if (cdrom) return cdrom->track_first_sector + cdrom->track_pregap_sectors; @@ -865,10 +823,11 @@ static uint32_t cdreader_first_track_sector(void* track_handle) void rc_hash_get_default_cdreader(struct rc_hash_cdreader* cdreader) { - cdreader->open_track = cdreader_open_track; + cdreader->open_track = NULL; cdreader->read_sector = cdreader_read_sector; cdreader->close_track = cdreader_close_track; cdreader->first_track_sector = cdreader_first_track_sector; + cdreader->open_track_iterator = cdreader_open_track_iterator; } void rc_hash_init_default_cdreader(void) diff --git a/deps/rcheevos/src/rhash/hash.c b/deps/rcheevos/src/rhash/hash.c index a86be0ac7a..7a382bb31b 100644 --- a/deps/rcheevos/src/rhash/hash.c +++ b/deps/rcheevos/src/rhash/hash.c @@ -1,58 +1,133 @@ #include "rc_hash.h" +#include "rc_hash_internal.h" + #include "../rc_compat.h" -#include "aes.h" -#include "md5.h" - -#include -#include - #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #include #include #endif -/* arbitrary limit to prevent allocating and hashing large files */ -#define MAX_BUFFER_SIZE 64 * 1024 * 1024 +#include +#include const char* rc_path_get_filename(const char* path); -static int rc_hash_whole_file(char hash[33], const char* path); +static int rc_hash_from_file(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator); /* ===================================================== */ -static rc_hash_message_callback error_message_callback = NULL; -rc_hash_message_callback verbose_message_callback = NULL; +static rc_hash_message_callback_deprecated g_error_message_callback = NULL; +static rc_hash_message_callback_deprecated g_verbose_message_callback = NULL; -void rc_hash_init_error_message_callback(rc_hash_message_callback callback) +static void rc_hash_call_g_error_message_callback(const char* message, const rc_hash_iterator_t* iterator) { - error_message_callback = callback; + (void)iterator; + g_error_message_callback(message); } -int rc_hash_error(const char* message) +static void rc_hash_call_g_verbose_message_callback(const char* message, const rc_hash_iterator_t* iterator) { - if (error_message_callback) - error_message_callback(message); + (void)iterator; + g_verbose_message_callback(message); +} + +static void rc_hash_dispatch_message_va(const rc_hash_message_callback_func callback, + const rc_hash_iterator_t* iterator, const char* format, va_list args) +{ + char buffer[1024]; + +#ifdef __STDC_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 + + callback(buffer, iterator); +} + +void rc_hash_init_error_message_callback(rc_hash_message_callback_deprecated callback) +{ + g_error_message_callback = callback; +} + +static rc_hash_message_callback_func rc_hash_get_error_message_callback(const rc_hash_callbacks_t* callbacks) +{ + if (callbacks && callbacks->error_message) + return callbacks->error_message; + + if (g_error_message_callback) + return rc_hash_call_g_error_message_callback; + + if (callbacks && callbacks->verbose_message) + return callbacks->verbose_message; + + if (g_verbose_message_callback) + return rc_hash_call_g_verbose_message_callback; + + return NULL; +} + +int rc_hash_iterator_error(const rc_hash_iterator_t* iterator, const char* message) +{ + rc_hash_message_callback_func message_callback = rc_hash_get_error_message_callback(&iterator->callbacks); + + if (message_callback) + message_callback(message, iterator); return 0; } -void rc_hash_init_verbose_message_callback(rc_hash_message_callback callback) +int rc_hash_iterator_error_formatted(const rc_hash_iterator_t* iterator, const char* format, ...) { - verbose_message_callback = callback; + rc_hash_message_callback_func message_callback = rc_hash_get_error_message_callback(&iterator->callbacks); + + if (message_callback) { + va_list args; + va_start(args, format); + rc_hash_dispatch_message_va(message_callback, iterator, format, args); + va_end(args); + } + + return 0; } -static void rc_hash_verbose(const char* message) +void rc_hash_init_verbose_message_callback(rc_hash_message_callback_deprecated callback) { - if (verbose_message_callback) - verbose_message_callback(message); + g_verbose_message_callback = callback; +} + +void rc_hash_iterator_verbose(const rc_hash_iterator_t* iterator, const char* message) +{ + if (iterator->callbacks.verbose_message) + iterator->callbacks.verbose_message(message, iterator); + else if (g_verbose_message_callback) + g_verbose_message_callback(message); +} + +void rc_hash_iterator_verbose_formatted(const rc_hash_iterator_t* iterator, const char* format, ...) +{ + if (iterator->callbacks.verbose_message) { + va_list args; + va_start(args, format); + rc_hash_dispatch_message_va(iterator->callbacks.verbose_message, iterator, format, args); + va_end(args); + } + else if (g_verbose_message_callback) { + va_list args; + va_start(args, format); + rc_hash_dispatch_message_va(rc_hash_call_g_verbose_message_callback, iterator, format, args); + va_end(args); + } } /* ===================================================== */ -static struct rc_hash_filereader filereader_funcs; -static struct rc_hash_filereader* filereader = NULL; +static struct rc_hash_filereader g_filereader_funcs; +static struct rc_hash_filereader* g_filereader = NULL; #if defined(WINVER) && WINVER >= 0x0500 static void* filereader_open(const char* path) @@ -72,13 +147,12 @@ static void* filereader_open(const char* path) if (!wpath) return NULL; - if (MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, wpath_length) == 0) - { + if (MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, wpath_length) == 0) { free(wpath); return NULL; } - #if defined(__STDC_WANT_SECURE_LIB__) + #if defined(__STDC_SECURE_LIB__) /* have to use _SH_DENYNO because some cores lock the file while its loaded */ fp = _wfsopen(wpath, L"rb", _SH_DENYNO); #else @@ -91,7 +165,7 @@ static void* filereader_open(const char* path) #else /* !WINVER >= 0x0500 */ static void* filereader_open(const char* path) { - #if defined(__STDC_WANT_SECURE_LIB__) + #if defined(__STDC_SECURE_LIB__) #if defined(WINVER) /* have to use _SH_DENYNO because some cores lock the file while its loaded */ return _fsopen(path, "rb", _SH_DENYNO); @@ -100,7 +174,7 @@ static void* filereader_open(const char* path) fopen_s(&fp, path, "rb"); return fp; #endif - #else /* !__STDC_WANT_SECURE_LIB__ */ + #else /* !__STDC_SECURE_LIB__ */ return fopen(path, "rb"); #endif } @@ -141,254 +215,93 @@ static void filereader_close(void* file_handle) /* for unit tests - normally would call rc_hash_init_custom_filereader(NULL) */ void rc_hash_reset_filereader(void) { - filereader = NULL; + g_filereader = NULL; } void rc_hash_init_custom_filereader(struct rc_hash_filereader* reader) { /* initialize with defaults first */ - filereader_funcs.open = filereader_open; - filereader_funcs.seek = filereader_seek; - filereader_funcs.tell = filereader_tell; - filereader_funcs.read = filereader_read; - filereader_funcs.close = filereader_close; + g_filereader_funcs.open = filereader_open; + g_filereader_funcs.seek = filereader_seek; + g_filereader_funcs.tell = filereader_tell; + g_filereader_funcs.read = filereader_read; + g_filereader_funcs.close = filereader_close; /* hook up any provided custom handlers */ if (reader) { if (reader->open) - filereader_funcs.open = reader->open; + g_filereader_funcs.open = reader->open; if (reader->seek) - filereader_funcs.seek = reader->seek; + g_filereader_funcs.seek = reader->seek; if (reader->tell) - filereader_funcs.tell = reader->tell; + g_filereader_funcs.tell = reader->tell; if (reader->read) - filereader_funcs.read = reader->read; + g_filereader_funcs.read = reader->read; if (reader->close) - filereader_funcs.close = reader->close; + g_filereader_funcs.close = reader->close; } - filereader = &filereader_funcs; + g_filereader = &g_filereader_funcs; } -void* rc_file_open(const char* path) +void* rc_file_open(const rc_hash_iterator_t* iterator, const char* path) { - void* handle; + void* handle = NULL; - if (!filereader) - { - rc_hash_init_custom_filereader(NULL); - if (!filereader) - return NULL; - } - - handle = filereader->open(path); - if (handle && verbose_message_callback) - { - char message[1024]; - snprintf(message, sizeof(message), "Opened %s", rc_path_get_filename(path)); - verbose_message_callback(message); + if (!iterator->callbacks.filereader.open) { + rc_hash_iterator_error(iterator, "No callback registered for opening files"); + } else { + handle = iterator->callbacks.filereader.open(path); + if (handle) + rc_hash_iterator_verbose_formatted(iterator, "Opened %s", rc_path_get_filename(path)); } return handle; } -void rc_file_seek(void* file_handle, int64_t offset, int origin) +void rc_file_seek(const rc_hash_iterator_t* iterator, void* file_handle, int64_t offset, int origin) { - if (filereader) - filereader->seek(file_handle, offset, origin); + if (iterator->callbacks.filereader.seek) + iterator->callbacks.filereader.seek(file_handle, offset, origin); } -int64_t rc_file_tell(void* file_handle) +int64_t rc_file_tell(const rc_hash_iterator_t* iterator, void* file_handle) { - return (filereader) ? filereader->tell(file_handle) : 0; + return iterator->callbacks.filereader.tell ? iterator->callbacks.filereader.tell(file_handle) : 0; } -size_t rc_file_read(void* file_handle, void* buffer, int requested_bytes) +size_t rc_file_read(const rc_hash_iterator_t* iterator, void* file_handle, void* buffer, int requested_bytes) { - return (filereader) ? filereader->read(file_handle, buffer, requested_bytes) : 0; + return iterator->callbacks.filereader.read ? iterator->callbacks.filereader.read(file_handle, buffer, requested_bytes) : 0; } -void rc_file_close(void* file_handle) +void rc_file_close(const rc_hash_iterator_t* iterator, void* file_handle) { - if (filereader) - filereader->close(file_handle); + if (iterator->callbacks.filereader.close) + iterator->callbacks.filereader.close(file_handle); } -/* ===================================================== */ - -static struct rc_hash_cdreader cdreader_funcs; -struct rc_hash_cdreader* cdreader = NULL; - -void rc_hash_init_custom_cdreader(struct rc_hash_cdreader* reader) +int64_t rc_file_size(const rc_hash_iterator_t* iterator, const char* path) { - if (reader) - { - memcpy(&cdreader_funcs, reader, sizeof(cdreader_funcs)); - cdreader = &cdreader_funcs; - } - else - { - cdreader = NULL; - } -} + int64_t size = 0; -static void* rc_cd_open_track(const char* path, uint32_t track) -{ - if (cdreader && cdreader->open_track) - return cdreader->open_track(path, track); - - rc_hash_error("no hook registered for cdreader_open_track"); - return NULL; -} - -static size_t rc_cd_read_sector(void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) -{ - if (cdreader && cdreader->read_sector) - return cdreader->read_sector(track_handle, sector, buffer, requested_bytes); - - rc_hash_error("no hook registered for cdreader_read_sector"); - return 0; -} - -static uint32_t rc_cd_first_track_sector(void* track_handle) -{ - if (cdreader && cdreader->first_track_sector) - return cdreader->first_track_sector(track_handle); - - rc_hash_error("no hook registered for cdreader_first_track_sector"); - return 0; -} - -static void rc_cd_close_track(void* track_handle) -{ - if (cdreader && cdreader->close_track) - { - cdreader->close_track(track_handle); - return; - } - - rc_hash_error("no hook registered for cdreader_close_track"); -} - -static uint32_t rc_cd_find_file_sector(void* track_handle, const char* path, uint32_t* size) -{ - uint8_t buffer[2048], *tmp; - int sector; - uint32_t 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) - { - /* find the directory record for the first part of the path */ - memcpy(buffer, path, slash - path); - buffer[slash - path] = '\0'; - - sector = rc_cd_find_file_sector(track_handle, (const char *)buffer, NULL); - if (!sector) - return 0; - - ++slash; - filename_length -= (slash - path); - path = slash; - } - else - { - uint32_t 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; - - /* the directory_record starts at 156, the sector containing the table of contents is 2 bytes into that. - * 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; + /* don't use rc_file_open to avoid log statements */ + if (!iterator->callbacks.filereader.open) { + rc_hash_iterator_error(iterator, "No callback registered for opening files"); + } else { + void* handle = iterator->callbacks.filereader.open(path); + if (handle) { + rc_file_seek(iterator, handle, 0, SEEK_END); + size = rc_file_tell(iterator, handle); + rc_file_close(iterator, handle); } } - /* fetch and process the directory record */ - if (!rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer))) - return 0; - - tmp = buffer; - do - { - 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[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) - { - char message[128]; - snprintf(message, sizeof(message), "Found %s at sector %d", path, sector); - verbose_message_callback(message); - } - - if (size) - *size = tmp[10] | (tmp[11] << 8) | (tmp[12] << 16) | (tmp[13] << 24); - - return sector; - } - - /* the first byte of the record is the length of the record */ - tmp += *tmp; - } while (1); - - return 0; -} - -/* ===================================================== */ - -static rc_hash_3ds_get_cia_normal_key_func _3ds_get_cia_normal_key_func = NULL; -static rc_hash_3ds_get_ncch_normal_keys_func _3ds_get_ncch_normal_keys_func = NULL; - -void rc_hash_init_3ds_get_cia_normal_key_func(rc_hash_3ds_get_cia_normal_key_func func) -{ - _3ds_get_cia_normal_key_func = func; -} - -void rc_hash_init_3ds_get_ncch_normal_keys_func(rc_hash_3ds_get_ncch_normal_keys_func func) -{ - _3ds_get_ncch_normal_keys_func = func; + return size; } /* ===================================================== */ @@ -396,8 +309,7 @@ void rc_hash_init_3ds_get_ncch_normal_keys_func(rc_hash_3ds_get_ncch_normal_keys const char* rc_path_get_filename(const char* path) { const char* ptr = path + strlen(path); - do - { + do { if (ptr[-1] == '/' || ptr[-1] == '\\') break; @@ -407,11 +319,10 @@ const char* rc_path_get_filename(const char* path) return ptr; } -static const char* rc_path_get_extension(const char* path) +const char* rc_path_get_extension(const char* path) { const char* ptr = path + strlen(path); - do - { + do { if (ptr[-1] == '.') return ptr; @@ -432,8 +343,7 @@ int rc_path_compare_extension(const char* path, const char* ext) if (memcmp(ptr, ext, ext_len) == 0) return 1; - do - { + do { if (tolower(*ptr) != *ext) return 0; @@ -446,12 +356,11 @@ int rc_path_compare_extension(const char* path, const char* ext) /* ===================================================== */ -static void rc_hash_byteswap16(uint8_t* buffer, const uint8_t* stop) +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) - { + while (ptr < stop32) { uint32_t temp = *ptr; temp = (temp & 0xFF00FF00) >> 8 | (temp & 0x00FF00FF) << 8; @@ -459,12 +368,11 @@ static void rc_hash_byteswap16(uint8_t* buffer, const uint8_t* stop) } } -static void rc_hash_byteswap32(uint8_t* buffer, const uint8_t* stop) +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) - { + while (ptr < stop32) { uint32_t temp = *ptr; temp = (temp & 0xFF000000) >> 24 | (temp & 0x00FF0000) >> 8 | @@ -474,7 +382,7 @@ static void rc_hash_byteswap32(uint8_t* buffer, const uint8_t* stop) } } -static int rc_hash_finalize(md5_state_t* md5, char hash[33]) +int rc_hash_finalize(const rc_hash_iterator_t* iterator, md5_state_t* md5, char hash[33]) { md5_byte_t digest[16]; @@ -486,2520 +394,25 @@ static int rc_hash_finalize(md5_state_t* md5, char hash[33]) digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15] ); - if (verbose_message_callback) - { - char message[128]; - snprintf(message, sizeof(message), "Generated hash %s", hash); - verbose_message_callback(message); - } + rc_hash_iterator_verbose_formatted(iterator, "Generated hash %s", hash); return 1; } -static int rc_hash_buffer(char hash[33], const uint8_t* buffer, size_t buffer_size) +int rc_hash_buffer(char hash[33], const uint8_t* buffer, size_t buffer_size, const rc_hash_iterator_t* iterator) { md5_state_t md5; - md5_init(&md5); if (buffer_size > MAX_BUFFER_SIZE) buffer_size = MAX_BUFFER_SIZE; + md5_init(&md5); + md5_append(&md5, buffer, (int)buffer_size); - if (verbose_message_callback) - { - char message[128]; - snprintf(message, sizeof(message), "Hashing %u byte buffer", (unsigned)buffer_size); - verbose_message_callback(message); - } + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte buffer", (unsigned)buffer_size); - return rc_hash_finalize(&md5, hash); -} - -static int rc_hash_cd_file(md5_state_t* md5, void* track_handle, uint32_t sector, const char* name, uint32_t size, const char* description) -{ - uint8_t buffer[2048]; - size_t num_read; - - if ((num_read = rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer))) < sizeof(buffer)) - { - char message[128]; - snprintf(message, sizeof(message), "Could not read %s", description); - return rc_hash_error(message); - } - - if (size > MAX_BUFFER_SIZE) - size = MAX_BUFFER_SIZE; - - if (verbose_message_callback) - { - char message[128]; - if (name) - snprintf(message, sizeof(message), "Hashing %s title (%u bytes) and contents (%u bytes) ", name, (unsigned)strlen(name), size); - else - snprintf(message, sizeof(message), "Hashing %s contents (%u bytes @ sector %u)", description, size, sector); - - verbose_message_callback(message); - } - - if (size < (unsigned)num_read) /* we read a whole sector - only hash the part containing file data */ - num_read = (size_t)size; - - do - { - md5_append(md5, buffer, (int)num_read); - - if (size <= (unsigned)num_read) - break; - size -= (unsigned)num_read; - - ++sector; - if (size >= sizeof(buffer)) - num_read = rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); - else - num_read = rc_cd_read_sector(track_handle, sector, buffer, size); - } while (num_read > 0); - - return 1; -} - -static int rc_hash_3do(char hash[33], const char* path) -{ - uint8_t buffer[2048]; - const uint8_t operafs_identifier[7] = { 0x01, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x01 }; - void* track_handle; - md5_state_t md5; - int sector; - int block_size, block_location; - int offset, stop; - size_t size = 0; - - track_handle = rc_cd_open_track(path, 1); - if (!track_handle) - return rc_hash_error("Could not open track"); - - /* the Opera filesystem stores the volume information in the first 132 bytes of sector 0 - * https://github.com/barbeque/3dodump/blob/master/OperaFS-Format.md - */ - rc_cd_read_sector(track_handle, 0, buffer, 132); - - if (memcmp(buffer, operafs_identifier, sizeof(operafs_identifier)) == 0) - { - if (verbose_message_callback) - { - char message[128]; - snprintf(message, sizeof(message), "Found 3DO CD, title=%.32s", &buffer[0x28]); - verbose_message_callback(message); - } - - /* include the volume header in the hash */ - md5_init(&md5); - md5_append(&md5, buffer, 132); - - /* the block size is at offset 0x4C (assume 0x4C is always 0) */ - block_size = buffer[0x4D] * 65536 + buffer[0x4E] * 256 + buffer[0x4F]; - - /* the root directory block location is at offset 0x64 (and duplicated several - * times, but we just look at the primary record) (assume 0x64 is always 0)*/ - block_location = buffer[0x65] * 65536 + buffer[0x66] * 256 + buffer[0x67]; - - /* multiply the block index by the block size to get the real address */ - block_location *= block_size; - - /* convert that to a sector and read it */ - sector = block_location / 2048; - - do - { - rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); - - /* offset to start of entries is at offset 0x10 (assume 0x10 and 0x11 are always 0) */ - offset = buffer[0x12] * 256 + buffer[0x13]; - - /* offset to end of entries is at offset 0x0C (assume 0x0C is always 0) */ - stop = buffer[0x0D] * 65536 + buffer[0x0E] * 256 + buffer[0x0F]; - - while (offset < stop) - { - if (buffer[offset + 0x03] == 0x02) /* file */ - { - if (strcasecmp((const char*)&buffer[offset + 0x20], "LaunchMe") == 0) - { - /* the block size is at offset 0x0C (assume 0x0C is always 0) */ - block_size = buffer[offset + 0x0D] * 65536 + buffer[offset + 0x0E] * 256 + buffer[offset + 0x0F]; - - /* the block location is at offset 0x44 (assume 0x44 is always 0) */ - block_location = buffer[offset + 0x45] * 65536 + buffer[offset + 0x46] * 256 + buffer[offset + 0x47]; - block_location *= block_size; - - /* the file size is at offset 0x10 (assume 0x10 is always 0) */ - size = (size_t)buffer[offset + 0x11] * 65536 + buffer[offset + 0x12] * 256 + buffer[offset + 0x13]; - - if (verbose_message_callback) - { - char message[128]; - snprintf(message, sizeof(message), "Hashing header (%u bytes) and %.32s (%u bytes) ", 132, &buffer[offset + 0x20], (unsigned)size); - verbose_message_callback(message); - } - - break; - } - } - - /* the number of extra copies of the file is at offset 0x40 (assume 0x40-0x42 are always 0) */ - offset += 0x48 + buffer[offset + 0x43] * 4; - } - - if (size != 0) - break; - - /* did not find the file, see if the directory listing is continued in another sector */ - offset = buffer[0x02] * 256 + buffer[0x03]; - - /* no more sectors to search*/ - if (offset == 0xFFFF) - break; - - /* get next sector */ - offset *= block_size; - sector = (block_location + offset) / 2048; - } while (1); - - if (size == 0) - { - rc_cd_close_track(track_handle); - return rc_hash_error("Could not find LaunchMe"); - } - - sector = block_location / 2048; - - while (size > 2048) - { - rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); - md5_append(&md5, buffer, sizeof(buffer)); - - ++sector; - size -= 2048; - } - - rc_cd_read_sector(track_handle, sector, buffer, size); - md5_append(&md5, buffer, (int)size); - } - else - { - rc_cd_close_track(track_handle); - return rc_hash_error("Not a 3DO CD"); - } - - rc_cd_close_track(track_handle); - - return rc_hash_finalize(&md5, hash); -} - -static int rc_hash_7800(char hash[33], const uint8_t* buffer, size_t buffer_size) -{ - /* if the file contains a header, ignore it */ - if (memcmp(&buffer[1], "ATARI7800", 9) == 0) - { - rc_hash_verbose("Ignoring 7800 header"); - - buffer += 128; - buffer_size -= 128; - } - - return rc_hash_buffer(hash, buffer, buffer_size); -} - -struct rc_hash_zip_idx -{ - size_t length; - uint8_t* data; -}; - -struct rc_hash_ms_dos_dosz_state -{ - const char* path; - const struct rc_hash_ms_dos_dosz_state* child; -}; - -static int rc_hash_zip_idx_sort(const void* a, const void* b) -{ - struct rc_hash_zip_idx *A = (struct rc_hash_zip_idx*)a, *B = (struct rc_hash_zip_idx*)b; - size_t len = (A->length < B->length ? A->length : B->length); - return memcmp(A->data, B->data, len); -} - -static int rc_hash_ms_dos_parent(md5_state_t* md5, const struct rc_hash_ms_dos_dosz_state *child, const char* parentname, uint32_t parentname_len); -static int rc_hash_ms_dos_dosc(md5_state_t* md5, const struct rc_hash_ms_dos_dosz_state *dosz); - -static int rc_hash_zip_file(md5_state_t* md5, void* file_handle, const struct rc_hash_ms_dos_dosz_state* dosz) -{ - uint8_t buf[2048], *alloc_buf, *cdir_start, *cdir_max, *cdir, *hashdata, eocdirhdr_size, cdirhdr_size, nparents; - uint32_t cdir_entry_len; - size_t sizeof_idx, indices_offset, alloc_size; - int64_t i_file, archive_size, ecdh_ofs, total_files, cdir_size, cdir_ofs; - struct rc_hash_zip_idx* hashindices, *hashindex; - - rc_file_seek(file_handle, 0, SEEK_END); - archive_size = rc_file_tell(file_handle); - - /* Basic sanity checks - reject files which are too small */ - eocdirhdr_size = 22; /* the 'end of central directory header' is 22 bytes */ - if (archive_size < eocdirhdr_size) - return rc_hash_error("ZIP is too small"); - - /* Macros used for reading ZIP and writing to a buffer for hashing (undefined again at the end of the function) */ - #define RC_ZIP_READ_LE16(p) ((uint16_t)(((const uint8_t*)(p))[0]) | ((uint16_t)(((const uint8_t*)(p))[1]) << 8U)) - #define RC_ZIP_READ_LE32(p) ((uint32_t)(((const uint8_t*)(p))[0]) | ((uint32_t)(((const uint8_t*)(p))[1]) << 8U) | ((uint32_t)(((const uint8_t*)(p))[2]) << 16U) | ((uint32_t)(((const uint8_t*)(p))[3]) << 24U)) - #define RC_ZIP_READ_LE64(p) ((uint64_t)(((const uint8_t*)(p))[0]) | ((uint64_t)(((const uint8_t*)(p))[1]) << 8U) | ((uint64_t)(((const uint8_t*)(p))[2]) << 16U) | ((uint64_t)(((const uint8_t*)(p))[3]) << 24U) | ((uint64_t)(((const uint8_t*)(p))[4]) << 32U) | ((uint64_t)(((const uint8_t*)(p))[5]) << 40U) | ((uint64_t)(((const uint8_t*)(p))[6]) << 48U) | ((uint64_t)(((const uint8_t*)(p))[7]) << 56U)) - #define RC_ZIP_WRITE_LE32(p,v) { ((uint8_t*)(p))[0] = (uint8_t)((uint32_t)(v) & 0xFF); ((uint8_t*)(p))[1] = (uint8_t)(((uint32_t)(v) >> 8) & 0xFF); ((uint8_t*)(p))[2] = (uint8_t)(((uint32_t)(v) >> 16) & 0xFF); ((uint8_t*)(p))[3] = (uint8_t)((uint32_t)(v) >> 24); } - #define RC_ZIP_WRITE_LE64(p,v) { ((uint8_t*)(p))[0] = (uint8_t)((uint64_t)(v) & 0xFF); ((uint8_t*)(p))[1] = (uint8_t)(((uint64_t)(v) >> 8) & 0xFF); ((uint8_t*)(p))[2] = (uint8_t)(((uint64_t)(v) >> 16) & 0xFF); ((uint8_t*)(p))[3] = (uint8_t)(((uint64_t)(v) >> 24) & 0xFF); ((uint8_t*)(p))[4] = (uint8_t)(((uint64_t)(v) >> 32) & 0xFF); ((uint8_t*)(p))[5] = (uint8_t)(((uint64_t)(v) >> 40) & 0xFF); ((uint8_t*)(p))[6] = (uint8_t)(((uint64_t)(v) >> 48) & 0xFF); ((uint8_t*)(p))[7] = (uint8_t)((uint64_t)(v) >> 56); } - - /* Find the end of central directory record by scanning the file from the end towards the beginning */ - for (ecdh_ofs = archive_size - sizeof(buf); ; ecdh_ofs -= (sizeof(buf) - 3)) - { - int i, n = sizeof(buf); - if (ecdh_ofs < 0) - ecdh_ofs = 0; - if (n > archive_size) - n = (int)archive_size; - rc_file_seek(file_handle, ecdh_ofs, SEEK_SET); - if (rc_file_read(file_handle, buf, n) != (size_t)n) - return rc_hash_error("ZIP read error"); - for (i = n - 4; i >= 0; --i) - if (RC_ZIP_READ_LE32(buf + i) == 0x06054b50) /* end of central directory header signature */ - break; - if (i >= 0) - { - ecdh_ofs += i; - break; - } - if (!ecdh_ofs || (archive_size - ecdh_ofs) >= (0xFFFF + eocdirhdr_size)) - return rc_hash_error("Failed to find ZIP central directory"); - } - - /* Read and verify the end of central directory record. */ - rc_file_seek(file_handle, ecdh_ofs, SEEK_SET); - if (rc_file_read(file_handle, buf, eocdirhdr_size) != eocdirhdr_size) - return rc_hash_error("Failed to read ZIP central directory"); - - /* Read central dir information from end of central directory header */ - total_files = RC_ZIP_READ_LE16(buf + 0x0A); - cdir_size = RC_ZIP_READ_LE32(buf + 0x0C); - cdir_ofs = RC_ZIP_READ_LE32(buf + 0x10); - - /* Check if this is a Zip64 file. In the block of code below: - * - 20 is the size of the ZIP64 end of central directory locator - * - 56 is the size of the ZIP64 end of central directory header - */ - if ((cdir_ofs == 0xFFFFFFFF || cdir_size == 0xFFFFFFFF || total_files == 0xFFFF) && ecdh_ofs >= (20 + 56)) - { - /* Read the ZIP64 end of central directory locator if it actually exists */ - rc_file_seek(file_handle, ecdh_ofs - 20, SEEK_SET); - if (rc_file_read(file_handle, buf, 20) == 20 && RC_ZIP_READ_LE32(buf) == 0x07064b50) /* locator signature */ - { - /* Found the locator, now read the actual ZIP64 end of central directory header */ - int64_t ecdh64_ofs = (int64_t)RC_ZIP_READ_LE64(buf + 0x08); - if (ecdh64_ofs <= (archive_size - 56)) - { - rc_file_seek(file_handle, ecdh64_ofs, SEEK_SET); - if (rc_file_read(file_handle, buf, 56) == 56 && RC_ZIP_READ_LE32(buf) == 0x06064b50) /* header signature */ - { - total_files = RC_ZIP_READ_LE64(buf + 0x20); - cdir_size = RC_ZIP_READ_LE64(buf + 0x28); - cdir_ofs = RC_ZIP_READ_LE64(buf + 0x30); - } - } - } - } - - /* Basic verificaton of central directory (limit to a 256MB content directory) */ - cdirhdr_size = 46; /* the 'central directory header' is 46 bytes */ - if ((cdir_size >= 0x10000000) || (cdir_size < total_files * cdirhdr_size) || ((cdir_ofs + cdir_size) > archive_size)) - return rc_hash_error("Central directory of ZIP file is invalid"); - - /* Allocate once for both directory and our temporary sort index (memory aligned to sizeof(rc_hash_zip_idx)) */ - sizeof_idx = sizeof(struct rc_hash_zip_idx); - indices_offset = (size_t)((cdir_size + sizeof_idx - 1) / sizeof_idx * sizeof_idx); - alloc_size = (size_t)(indices_offset + total_files * sizeof_idx); - alloc_buf = (uint8_t*)malloc(alloc_size); - - /* Read entire central directory to a buffer */ - if (!alloc_buf) - return rc_hash_error("Could not allocate temporary buffer"); - rc_file_seek(file_handle, cdir_ofs, SEEK_SET); - if ((int64_t)rc_file_read(file_handle, alloc_buf, (int)cdir_size) != cdir_size) - { - free(alloc_buf); - return rc_hash_error("Failed to read central directory of ZIP file"); - } - - cdir_start = alloc_buf; - cdir_max = cdir_start + cdir_size - cdirhdr_size; - cdir = cdir_start; - - /* Write our temporary hash data to the same buffer we read the central directory from. - * We can do that because the amount of data we keep for each file is guaranteed to be less than the file record. - */ - hashdata = alloc_buf; - hashindices = (struct rc_hash_zip_idx*)(alloc_buf + indices_offset); - hashindex = hashindices; - - /* Now process the central directory file records */ - for (i_file = nparents = 0, cdir = cdir_start; i_file < total_files && cdir >= cdir_start && cdir <= cdir_max; i_file++, cdir += cdir_entry_len) - { - const uint8_t *name, *name_end; - uint32_t signature = RC_ZIP_READ_LE32(cdir + 0x00); - uint32_t method = RC_ZIP_READ_LE16(cdir + 0x0A); - uint32_t crc32 = RC_ZIP_READ_LE32(cdir + 0x10); - uint64_t comp_size = RC_ZIP_READ_LE32(cdir + 0x14); - uint64_t decomp_size = RC_ZIP_READ_LE32(cdir + 0x18); - uint32_t filename_len = RC_ZIP_READ_LE16(cdir + 0x1C); - int32_t extra_len = RC_ZIP_READ_LE16(cdir + 0x1E); - int32_t comment_len = RC_ZIP_READ_LE16(cdir + 0x20); - int32_t external_attr = RC_ZIP_READ_LE16(cdir + 0x26); - uint64_t local_hdr_ofs = RC_ZIP_READ_LE32(cdir + 0x2A); - cdir_entry_len = cdirhdr_size + filename_len + extra_len + comment_len; - - if (signature != 0x02014b50) /* expected central directory entry signature */ - break; - - /* Ignore records describing a directory (we only hash file records) */ - name = (cdir + cdirhdr_size); - if (name[filename_len - 1] == '/' || name[filename_len - 1] == '\\' || (external_attr & 0x10)) - continue; - - /* Handle Zip64 fields */ - if (decomp_size == 0xFFFFFFFF || comp_size == 0xFFFFFFFF || local_hdr_ofs == 0xFFFFFFFF) - { - int invalid = 0; - const uint8_t *x = cdir + cdirhdr_size + filename_len, *xEnd, *field, *fieldEnd; - for (xEnd = x + extra_len; (x + (sizeof(uint16_t) * 2)) < xEnd; x = fieldEnd) - { - field = x + (sizeof(uint16_t) * 2); - fieldEnd = field + RC_ZIP_READ_LE16(x + 2); - if (RC_ZIP_READ_LE16(x) != 0x0001 || fieldEnd > xEnd) - continue; /* Not the Zip64 extended information extra field */ - - if (decomp_size == 0xFFFFFFFF) - { - if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { invalid = 1; break; } - decomp_size = RC_ZIP_READ_LE64(field); - field += sizeof(uint64_t); - } - if (comp_size == 0xFFFFFFFF) - { - if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { invalid = 1; break; } - comp_size = RC_ZIP_READ_LE64(field); - field += sizeof(uint64_t); - } - if (local_hdr_ofs == 0xFFFFFFFF) - { - if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { invalid = 1; break; } - local_hdr_ofs = RC_ZIP_READ_LE64(field); - field += sizeof(uint64_t); - } - break; - } - if (invalid) - { - free(alloc_buf); - return rc_hash_error("Encountered invalid Zip64 file"); - } - } - - /* Basic sanity check on file record */ - /* 30 is the length of the local directory header preceeding the compressed data */ - if ((!method && decomp_size != comp_size) || (decomp_size && !comp_size) || ((local_hdr_ofs + 30 + comp_size) > (uint64_t)archive_size)) - { - free(alloc_buf); - return rc_hash_error("Encountered invalid entry in ZIP central directory"); - } - - /* A DOSZ file can contain a special empty .dosz.parent file in its root which means a parent dosz file is used */ - if (dosz && decomp_size == 0 && filename_len > 7 && !strncasecmp((const char*)name + filename_len - 7, ".parent", 7) && !memchr(name, '/', filename_len) && !memchr(name, '\\', filename_len)) - { - /* A DOSZ file can only have one parent file */ - if (nparents++) - { - free(alloc_buf); - return rc_hash_error("Invalid DOSZ file with multiple parents"); - } - - /* If there is an error with the parent DOSZ, abort now */ - if (!rc_hash_ms_dos_parent(md5, dosz, (const char*)name, (filename_len - 7))) - { - free(alloc_buf); - return 0; - } - - /* We don't hash this meta file so a user is free to rename it and the parent file */ - continue; - } - - /* Write the pointer and length of the data we record about this file */ - hashindex->data = hashdata; - hashindex->length = filename_len + 1 + 4 + 8; - hashindex++; - - /* Convert and store the file name in the hash data buffer */ - for (name_end = name + filename_len; name != name_end; name++) - { - *(hashdata++) = - (*name == '\\' ? '/' : /* convert back-slashes to regular slashes */ - (*name >= 'A' && *name <= 'Z') ? (*name | 0x20) : /* convert upper case letters to lower case */ - *name); /* else use the byte as-is */ - } - - /* Add zero terminator, CRC32 and decompressed size to the hash data buffer */ - *(hashdata++) = '\0'; - RC_ZIP_WRITE_LE32(hashdata, crc32); - hashdata += 4; - RC_ZIP_WRITE_LE64(hashdata, decomp_size); - hashdata += 8; - - if (verbose_message_callback) - { - char message[1024]; - snprintf(message, sizeof(message), "File in ZIP: %.*s (%u bytes, CRC32 = %08X)", filename_len, (const char*)(cdir + cdirhdr_size), (unsigned)decomp_size, crc32); - verbose_message_callback(message); - } - } - - if (verbose_message_callback) - { - char message[1024]; - snprintf(message, sizeof(message), "Hashing %u files in ZIP archive", (unsigned)(hashindex - hashindices)); - verbose_message_callback(message); - } - - /* Sort the file list indices */ - qsort(hashindices, (hashindex - hashindices), sizeof(struct rc_hash_zip_idx), rc_hash_zip_idx_sort); - - /* Hash the data in the order of the now sorted indices */ - for (; hashindices != hashindex; hashindices++) - md5_append(md5, hashindices->data, (int)hashindices->length); - - free(alloc_buf); - - /* If this is a .dosz file, check if an associated .dosc file exists */ - if (dosz && !rc_hash_ms_dos_dosc(md5, dosz)) - return 0; - - return 1; - - #undef RC_ZIP_READ_LE16 - #undef RC_ZIP_READ_LE32 - #undef RC_ZIP_READ_LE64 - #undef RC_ZIP_WRITE_LE32 - #undef RC_ZIP_WRITE_LE64 -} - -static int rc_hash_ms_dos_parent(md5_state_t* md5, const struct rc_hash_ms_dos_dosz_state *child, const char* parentname, uint32_t parentname_len) -{ - const char *lastfslash = strrchr(child->path, '/'); - const char *lastbslash = strrchr(child->path, '\\'); - const char *lastslash = (lastbslash > lastfslash ? lastbslash : lastfslash); - size_t dir_len = (lastslash ? (lastslash + 1 - child->path) : 0); - char* parent_path = (char*)malloc(dir_len + parentname_len + 1); - struct rc_hash_ms_dos_dosz_state parent; - const struct rc_hash_ms_dos_dosz_state *check; - void* parent_handle; - int parent_res; - - /* Build the path of the parent by combining the directory of the current file with the name */ - if (!parent_path) - return rc_hash_error("Could not allocate temporary buffer"); - - memcpy(parent_path, child->path, dir_len); - memcpy(parent_path + dir_len, parentname, parentname_len); - parent_path[dir_len + parentname_len] = '\0'; - - /* Make sure there is no recursion where a parent DOSZ is an already seen child DOSZ */ - for (check = child->child; check; check = check->child) - { - if (!strcmp(check->path, parent_path)) - { - free(parent_path); - return rc_hash_error("Invalid DOSZ file with recursive parents"); - } - } - - /* Try to open the parent DOSZ file */ - parent_handle = rc_file_open(parent_path); - if (!parent_handle) - { - char message[1024]; - snprintf(message, sizeof(message), "DOSZ parent file '%s' does not exist", parent_path); - free(parent_path); - return rc_hash_error(message); - } - - /* Fully hash the parent DOSZ ahead of the child */ - parent.path = parent_path; - parent.child = child; - parent_res = rc_hash_zip_file(md5, parent_handle, &parent); - rc_file_close(parent_handle); - free(parent_path); - return parent_res; -} - -static int rc_hash_ms_dos_dosc(md5_state_t* md5, const struct rc_hash_ms_dos_dosz_state *dosz) -{ - size_t path_len = strlen(dosz->path); - if (dosz->path[path_len-1] == 'z' || dosz->path[path_len-1] == 'Z') - { - void* file_handle; - char *dosc_path = strdup(dosz->path); - if (!dosc_path) - return rc_hash_error("Could not allocate temporary buffer"); - - /* Swap the z to c and use the same capitalization, hash the file if it exists */ - dosc_path[path_len-1] = (dosz->path[path_len-1] == 'z' ? 'c' : 'C'); - file_handle = rc_file_open(dosc_path); - free(dosc_path); - - if (file_handle) - { - /* Hash the DOSC as a plain zip file (pass NULL as dosz state) */ - int res = rc_hash_zip_file(md5, file_handle, NULL); - rc_file_close(file_handle); - if (!res) - return 0; - } - } - return 1; -} - -static int rc_hash_ms_dos(char hash[33], const char* path) -{ - struct rc_hash_ms_dos_dosz_state dosz; - md5_state_t md5; - int res; - - void* file_handle = rc_file_open(path); - if (!file_handle) - return rc_hash_error("Could not open file"); - - /* hash the main content zip file first */ - md5_init(&md5); - dosz.path = path; - dosz.child = NULL; - res = rc_hash_zip_file(&md5, file_handle, &dosz); - rc_file_close(file_handle); - - if (!res) - return 0; - - return rc_hash_finalize(&md5, hash); -} - -static int rc_hash_arcade(char hash[33], const char* path) -{ - /* arcade hash is just the hash of the filename (no extension) - the cores are pretty stringent about having the right ROM data */ - const char* filename = rc_path_get_filename(path); - const char* ext = rc_path_get_extension(filename); - char buffer[128]; /* realistically, this should never need more than ~32 characters */ - size_t filename_length = ext - filename - 1; - - /* fbneo supports loading subsystems by using specific folder names. - * if one is found, include it in the hash. - * https://github.com/libretro/FBNeo/blob/master/src/burner/libretro/README.md#emulating-consoles-and-computers - */ - if (filename > path + 1) - { - int include_folder = 0; - const char* folder = filename - 1; - size_t parent_folder_length = 0; - - do - { - if (folder[-1] == '/' || folder[-1] == '\\') - break; - - --folder; - } while (folder > path); - - parent_folder_length = filename - folder - 1; - if (parent_folder_length < 16) - { - char* ptr = buffer; - while (folder < filename - 1) - *ptr++ = tolower(*folder++); - *ptr = '\0'; - - folder = buffer; - } - - switch (parent_folder_length) - { - case 3: - if (memcmp(folder, "nes", 3) == 0 || /* NES */ - memcmp(folder, "fds", 3) == 0 || /* FDS */ - memcmp(folder, "sms", 3) == 0 || /* Master System */ - memcmp(folder, "msx", 3) == 0 || /* MSX */ - memcmp(folder, "ngp", 3) == 0 || /* NeoGeo Pocket */ - memcmp(folder, "pce", 3) == 0 || /* PCEngine */ - memcmp(folder, "chf", 3) == 0 || /* ChannelF */ - memcmp(folder, "sgx", 3) == 0) /* SuperGrafX */ - include_folder = 1; - break; - case 4: - if (memcmp(folder, "tg16", 4) == 0 || /* TurboGrafx-16 */ - memcmp(folder, "msx1", 4) == 0) /* MSX */ - include_folder = 1; - break; - case 5: - if (memcmp(folder, "neocd", 5) == 0) /* NeoGeo CD */ - include_folder = 1; - break; - case 6: - if (memcmp(folder, "coleco", 6) == 0 || /* Colecovision */ - memcmp(folder, "sg1000", 6) == 0) /* SG-1000 */ - include_folder = 1; - break; - case 7: - if (memcmp(folder, "genesis", 7) == 0) /* Megadrive (Genesis) */ - include_folder = 1; - break; - case 8: - if (memcmp(folder, "gamegear", 8) == 0 || /* Game Gear */ - memcmp(folder, "megadriv", 8) == 0 || /* Megadrive */ - memcmp(folder, "pcengine", 8) == 0 || /* PCEngine */ - memcmp(folder, "channelf", 8) == 0 || /* ChannelF */ - memcmp(folder, "spectrum", 8) == 0) /* ZX Spectrum */ - include_folder = 1; - break; - case 9: - if (memcmp(folder, "megadrive", 9) == 0) /* Megadrive */ - include_folder = 1; - break; - case 10: - if (memcmp(folder, "supergrafx", 10) == 0 || /* SuperGrafX */ - memcmp(folder, "zxspectrum", 10) == 0) /* ZX Spectrum */ - include_folder = 1; - break; - case 12: - if (memcmp(folder, "mastersystem", 12) == 0 || /* Master System */ - memcmp(folder, "colecovision", 12) == 0) /* Colecovision */ - include_folder = 1; - break; - default: - break; - } - - if (include_folder) - { - if (parent_folder_length + filename_length + 1 < sizeof(buffer)) - { - buffer[parent_folder_length] = '_'; - memcpy(&buffer[parent_folder_length + 1], filename, filename_length); - return rc_hash_buffer(hash, (uint8_t*)&buffer[0], parent_folder_length + filename_length + 1); - } - } - } - - return rc_hash_buffer(hash, (uint8_t*)filename, filename_length); -} - -static int rc_hash_text(char hash[33], const uint8_t* buffer, size_t buffer_size) -{ - md5_state_t md5; - const uint8_t* scan = buffer; - const uint8_t* stop = buffer + buffer_size; - - md5_init(&md5); - - do { - /* find end of line */ - while (scan < stop && *scan != '\r' && *scan != '\n') - ++scan; - - md5_append(&md5, buffer, (int)(scan - buffer)); - - /* include a normalized line ending */ - /* NOTE: this causes a line ending to be hashed at the end of the file, even if one was not present */ - md5_append(&md5, (const uint8_t*)"\n", 1); - - /* skip newline */ - if (scan < stop && *scan == '\r') - ++scan; - if (scan < stop && *scan == '\n') - ++scan; - - buffer = scan; - } while (scan < stop); - - 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; - uint32_t size = 0; - uint32_t offset = 0; - uint32_t sector = 0; - uint32_t remaining; - uint32_t 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 */ - if (buffer[0] == 'L' && buffer[1] == 'Y' && buffer[2] == 'N' && buffer[3] == 'X' && buffer[4] == 0) - { - rc_hash_verbose("Ignoring LYNX header"); - - buffer += 64; - buffer_size -= 64; - } - - 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; - uint32_t 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 */ - if (buffer[0] == 'N' && buffer[1] == 'E' && buffer[2] == 'S' && buffer[3] == 0x1A) - { - rc_hash_verbose("Ignoring NES header"); - - buffer += 16; - buffer_size -= 16; - } - else if (buffer[0] == 'F' && buffer[1] == 'D' && buffer[2] == 'S' && buffer[3] == 0x1A) - { - rc_hash_verbose("Ignoring FDS header"); - - buffer += 16; - buffer_size -= 16; - } - - return rc_hash_buffer(hash, buffer, buffer_size); -} - -static int rc_hash_n64(char hash[33], const char* path) -{ - uint8_t* buffer; - uint8_t* stop; - const size_t buffer_size = 65536; - md5_state_t md5; - size_t remaining; - void* file_handle; - int is_v64 = 0; - int is_n64 = 0; - - file_handle = rc_file_open(path); - if (!file_handle) - return rc_hash_error("Could not open file"); - - buffer = (uint8_t*)malloc(buffer_size); - if (!buffer) - { - rc_file_close(file_handle); - return rc_hash_error("Could not allocate temporary buffer"); - } - stop = buffer + buffer_size; - - /* read first byte so we can detect endianness */ - rc_file_seek(file_handle, 0, SEEK_SET); - rc_file_read(file_handle, buffer, 1); - - if (buffer[0] == 0x80) /* z64 format (big endian [native]) */ - { - } - else if (buffer[0] == 0x37) /* v64 format (byteswapped) */ - { - rc_hash_verbose("converting v64 to z64"); - is_v64 = 1; - } - else if (buffer[0] == 0x40) /* n64 format (little endian) */ - { - 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); - rc_file_close(file_handle); - - rc_hash_verbose("Not a Nintendo 64 ROM"); - return 0; - } - - /* calculate total file size */ - rc_file_seek(file_handle, 0, SEEK_END); - remaining = (size_t)rc_file_tell(file_handle); - if (remaining > MAX_BUFFER_SIZE) - remaining = MAX_BUFFER_SIZE; - - if (verbose_message_callback) - { - char message[64]; - snprintf(message, sizeof(message), "Hashing %u bytes", (unsigned)remaining); - verbose_message_callback(message); - } - - /* begin hashing */ - md5_init(&md5); - - rc_file_seek(file_handle, 0, SEEK_SET); - while (remaining >= buffer_size) - { - rc_file_read(file_handle, buffer, (int)buffer_size); - - if (is_v64) - rc_hash_byteswap16(buffer, stop); - else if (is_n64) - rc_hash_byteswap32(buffer, stop); - - md5_append(&md5, buffer, (int)buffer_size); - remaining -= buffer_size; - } - - if (remaining > 0) - { - rc_file_read(file_handle, buffer, (int)remaining); - - stop = buffer + remaining; - if (is_v64) - rc_hash_byteswap16(buffer, stop); - else if (is_n64) - rc_hash_byteswap32(buffer, stop); - - md5_append(&md5, buffer, (int)remaining); - } - - /* cleanup */ - rc_file_close(file_handle); - free(buffer); - - return rc_hash_finalize(&md5, hash); -} - -static int rc_hash_nintendo_3ds_ncch(md5_state_t* md5, void* file_handle, uint8_t header[0x200], struct AES_ctx* cia_aes) -{ - struct AES_ctx ncch_aes; - uint8_t* hash_buffer; - uint64_t exefs_offset, exefs_real_size; - uint32_t exefs_buffer_size; - uint8_t primary_key[AES_KEYLEN], secondary_key[AES_KEYLEN]; - uint8_t fixed_key_flag, no_crypto_flag, seed_crypto_flag; - uint8_t crypto_method, secondary_key_x_slot; - uint16_t ncch_version; - uint32_t i; - uint8_t primary_key_y[AES_KEYLEN], program_id[sizeof(uint64_t)]; - uint8_t iv[AES_BLOCKLEN]; - uint8_t exefs_section_name[8]; - uint64_t exefs_section_offset, exefs_section_size; - - exefs_offset = ((uint32_t)header[0x1A3] << 24) | (header[0x1A2] << 16) | (header[0x1A1] << 8) | header[0x1A0]; - exefs_real_size = ((uint32_t)header[0x1A7] << 24) | (header[0x1A6] << 16) | (header[0x1A5] << 8) | header[0x1A4]; - - /* Offset and size are in "media units" (1 media unit = 0x200 bytes) */ - exefs_offset *= 0x200; - exefs_real_size *= 0x200; - - if (exefs_real_size > MAX_BUFFER_SIZE) - exefs_buffer_size = MAX_BUFFER_SIZE; - else - exefs_buffer_size = (uint32_t)exefs_real_size; - - /* This region is technically optional, but it should always be present for executable content (i.e. games) */ - if (exefs_offset == 0 || exefs_real_size == 0) - return rc_hash_error("ExeFS was not available"); - - /* NCCH flag 7 is a bitfield of various crypto related flags */ - fixed_key_flag = header[0x188 + 7] & 0x01; - no_crypto_flag = header[0x188 + 7] & 0x04; - seed_crypto_flag = header[0x188 + 7] & 0x20; - - ncch_version = (header[0x113] << 8) | header[0x112]; - - if (no_crypto_flag == 0) - { - rc_hash_verbose("Encrypted NCCH detected"); - - if (fixed_key_flag != 0) - { - /* Fixed crypto key means all 0s for both keys */ - memset(primary_key, 0, sizeof(primary_key)); - memset(secondary_key, 0, sizeof(secondary_key)); - rc_hash_verbose("Using fixed key crypto"); - } - else - { - if (_3ds_get_ncch_normal_keys_func == NULL) - return rc_hash_error("An encrypted NCCH was detected, but the NCCH normal keys callback was not set"); - - /* Primary key y is just the first 16 bytes of the header */ - memcpy(primary_key_y, header, sizeof(primary_key_y)); - - /* NCCH flag 3 indicates which secondary key x slot is used */ - crypto_method = header[0x188 + 3]; - - switch (crypto_method) - { - case 0x00: - rc_hash_verbose("Using NCCH crypto method v1"); - secondary_key_x_slot = 0x2C; - break; - case 0x01: - rc_hash_verbose("Using NCCH crypto method v2"); - secondary_key_x_slot = 0x25; - break; - case 0x0A: - rc_hash_verbose("Using NCCH crypto method v3"); - secondary_key_x_slot = 0x18; - break; - case 0x0B: - rc_hash_verbose("Using NCCH crypto method v4"); - secondary_key_x_slot = 0x1B; - break; - default: - snprintf((char*)header, 0x200, "Invalid crypto method %02X", (unsigned)crypto_method); - return rc_hash_error((const char*)header); - } - - /* We only need the program id if we're doing seed crypto */ - if (seed_crypto_flag != 0) - { - rc_hash_verbose("Using seed crypto"); - memcpy(program_id, &header[0x118], sizeof(program_id)); - } - - if (_3ds_get_ncch_normal_keys_func(primary_key_y, secondary_key_x_slot, seed_crypto_flag != 0 ? program_id : NULL, primary_key, secondary_key) == 0) - return rc_hash_error("Could not obtain NCCH normal keys"); - } - - switch (ncch_version) - { - case 0: - case 2: - rc_hash_verbose("Detected NCCH version 0/2"); - for (i = 0; i < 8; i++) - { - /* First 8 bytes is the partition id in reverse byte order */ - iv[7 - i] = header[0x108 + i]; - } - - /* Magic number for ExeFS */ - iv[8] = 2; - - /* Rest of the bytes are 0 */ - memset(&iv[9], 0, sizeof(iv) - 9); - break; - case 1: - rc_hash_verbose("Detected NCCH version 1"); - for (i = 0; i < 8; i++) - { - /* First 8 bytes is the partition id in normal byte order */ - iv[i] = header[0x108 + i]; - } - - /* Next 4 bytes are 0 */ - memset(&iv[8], 0, 4); - - /* Last 4 bytes is the ExeFS byte offset in big endian */ - iv[12] = (exefs_offset >> 24) & 0xFF; - iv[13] = (exefs_offset >> 16) & 0xFF; - iv[14] = (exefs_offset >> 8) & 0xFF; - iv[15] = exefs_offset & 0xFF; - break; - default: - snprintf((char*)header, 0x200, "Invalid NCCH version %04X", (unsigned)ncch_version); - return rc_hash_error((const char*)header); - } - } - - /* ASSERT: file position must be +0x200 from start of NCCH (i.e. end of header) */ - exefs_offset -= 0x200; - - if (cia_aes) - { - /* We have to decrypt the data between the header and the ExeFS so the CIA AES state is correct - * when we reach the ExeFS. This decrypted data is not included in the RetroAchievements hash */ - - /* This should never happen in practice, but just in case */ - if (exefs_offset > MAX_BUFFER_SIZE) - return rc_hash_error("Too much data required to decrypt in order to hash"); - - hash_buffer = (uint8_t*)malloc((uint32_t)exefs_offset); - if (!hash_buffer) - { - snprintf((char*)header, 0x200, "Failed to allocate %u bytes", (unsigned)exefs_offset); - return rc_hash_error((const char*)header); - } - - if (rc_file_read(file_handle, hash_buffer, (uint32_t)exefs_offset) != (uint32_t)exefs_offset) - { - free(hash_buffer); - return rc_hash_error("Could not read NCCH data"); - } - - AES_CBC_decrypt_buffer(cia_aes, hash_buffer, (uint32_t)exefs_offset); - free(hash_buffer); - } - else - { - /* No decryption needed, just skip over the in-between data */ - rc_file_seek(file_handle, (int64_t)exefs_offset, SEEK_CUR); - } - - hash_buffer = (uint8_t*)malloc(exefs_buffer_size); - if (!hash_buffer) - { - snprintf((char*)header, 0x200, "Failed to allocate %u bytes", (unsigned)exefs_buffer_size); - return rc_hash_error((const char*)header); - } - - /* Clear out crypto flags to ensure we get the same hash for decrypted and encrypted ROMs */ - memset(&header[0x114], 0, 4); - header[0x188 + 3] = 0; - header[0x188 + 7] &= ~(0x20 | 0x04 | 0x01); - - rc_hash_verbose("Hashing 512 byte NCCH header"); - md5_append(md5, header, 0x200); - - if (verbose_message_callback) - { - snprintf((char*)header, 0x200, "Hashing %u bytes for ExeFS (at NCCH offset %08X%08X)", (unsigned)exefs_buffer_size, (unsigned)(exefs_offset >> 32), (unsigned)exefs_offset); - verbose_message_callback((const char*)header); - } - - if (rc_file_read(file_handle, hash_buffer, exefs_buffer_size) != exefs_buffer_size) - { - free(hash_buffer); - return rc_hash_error("Could not read ExeFS data"); - } - - if (cia_aes) - { - rc_hash_verbose("Performing CIA decryption for ExeFS"); - AES_CBC_decrypt_buffer(cia_aes, hash_buffer, exefs_buffer_size); - } - - if (no_crypto_flag == 0) - { - rc_hash_verbose("Performing NCCH decryption for ExeFS"); - - AES_init_ctx_iv(&ncch_aes, primary_key, iv); - AES_CTR_xcrypt_buffer(&ncch_aes, hash_buffer, 0x200); - - for (i = 0; i < 8; i++) - { - memcpy(exefs_section_name, &hash_buffer[i * 16], sizeof(exefs_section_name)); - exefs_section_offset = ((uint32_t)hash_buffer[i * 16 + 11] << 24) | (hash_buffer[i * 16 + 10] << 16) | (hash_buffer[i * 16 + 9] << 8) | hash_buffer[i * 16 + 8]; - exefs_section_size = ((uint32_t)hash_buffer[i * 16 + 15] << 24) | (hash_buffer[i * 16 + 14] << 16) | (hash_buffer[i * 16 + 13] << 8) | hash_buffer[i * 16 + 12]; - - /* 0 size indicates an unused section */ - if (exefs_section_size == 0) - continue; - - /* Offsets must be aligned by a media unit */ - if (exefs_section_offset & 0x1FF) - return rc_hash_error("ExeFS section offset is misaligned"); - - /* Offset is relative to the end of the header */ - exefs_section_offset += 0x200; - - /* Check against malformed sections */ - if (exefs_section_offset + ((exefs_section_size + 0x1FF) & ~(uint64_t)0x1FF) > (uint64_t)exefs_real_size) - return rc_hash_error("ExeFS section would overflow"); - - if (memcmp(exefs_section_name, "icon", 4) == 0 || memcmp(exefs_section_name, "banner", 6) == 0) - { - /* Align size up by a media unit */ - exefs_section_size = (exefs_section_size + 0x1FF) & ~(uint64_t)0x1FF; - AES_init_ctx(&ncch_aes, primary_key); - } - else - { - /* We don't align size up here, as the padding bytes will use the primary key rather than the secondary key */ - AES_init_ctx(&ncch_aes, secondary_key); - } - - /* In theory, the section offset + size could be greater than the buffer size */ - /* In practice, this likely never occurs, but just in case it does, ignore the section or constrict the size */ - if (exefs_section_offset + exefs_section_size > exefs_buffer_size) - { - if (exefs_section_offset >= exefs_buffer_size) - continue; - - exefs_section_size = exefs_buffer_size - exefs_section_offset; - } - - if (verbose_message_callback) - { - exefs_section_name[7] = '\0'; - snprintf((char*)header, 0x200, "Decrypting ExeFS file %s at ExeFS offset %08X with size %08X", (const char*)exefs_section_name, (unsigned)exefs_section_offset, (unsigned)exefs_section_size); - verbose_message_callback((const char*)header); - } - - AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], exefs_section_size & ~(uint64_t)0xF); - - if (exefs_section_size & 0x1FF) - { - /* Handle padding bytes, these always use the primary key */ - exefs_section_offset += exefs_section_size; - exefs_section_size = 0x200 - (exefs_section_size & 0x1FF); - - if (verbose_message_callback) - { - snprintf((char*)header, 0x200, "Decrypting ExeFS padding at ExeFS offset %08X with size %08X", (unsigned)exefs_section_offset, (unsigned)exefs_section_size); - verbose_message_callback((const char*)header); - } - - /* Align our decryption start to an AES block boundary */ - if (exefs_section_size & 0xF) - { - /* We're a little evil here re-using the IV like this, but this seems to be the best way to deal with this... */ - memcpy(iv, ncch_aes.Iv, sizeof(iv)); - exefs_section_offset &= ~(uint64_t)0xF; - - /* First decrypt these last bytes using the secondary key */ - AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], 0x10 - (exefs_section_size & 0xF)); - - /* Now re-encrypt these bytes using the primary key */ - AES_init_ctx_iv(&ncch_aes, primary_key, iv); - AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], 0x10 - (exefs_section_size & 0xF)); - - /* All of the padding can now be decrypted using the primary key */ - AES_ctx_set_iv(&ncch_aes, iv); - exefs_section_size += 0x10 - (exefs_section_size & 0xF); - } - - AES_init_ctx(&ncch_aes, primary_key); - AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], (size_t)exefs_section_size); - } - } - } - - md5_append(md5, hash_buffer, exefs_buffer_size); - - free(hash_buffer); - return 1; -} - -static uint32_t rc_hash_nintendo_3ds_cia_signature_size(uint8_t header[0x200]) -{ - uint32_t signature_type; - - signature_type = ((uint32_t)header[0] << 24) | (header[1] << 16) | (header[2] << 8) | header[3]; - switch (signature_type) - { - case 0x010000: - case 0x010003: - return 0x200 + 0x3C; - case 0x010001: - case 0x010004: - return 0x100 + 0x3C; - case 0x010002: - case 0x010005: - return 0x3C + 0x40; - default: - snprintf((char*)header, 0x200, "Invalid signature type %08X", (unsigned)signature_type); - return rc_hash_error((const char*)header); - } -} - -static int rc_hash_nintendo_3ds_cia(md5_state_t* md5, void* file_handle, uint8_t header[0x200]) -{ - const uint32_t CIA_HEADER_SIZE = 0x2020; /* Yes, this is larger than the header[0x200], but we only use the beginning of the header */ - const uint64_t CIA_ALIGNMENT_MASK = 64 - 1; /* sizes are aligned by 64 bytes */ - struct AES_ctx aes; - uint8_t iv[AES_BLOCKLEN], normal_key[AES_KEYLEN], title_key[AES_KEYLEN], title_id[sizeof(uint64_t)]; - uint32_t cert_size, tik_size, tmd_size; - int64_t cert_offset, tik_offset, tmd_offset, content_offset; - uint32_t signature_size, i; - uint16_t content_count; - uint8_t common_key_index; - - cert_size = ((uint32_t)header[0x0B] << 24) | (header[0x0A] << 16) | (header[0x09] << 8) | header[0x08]; - tik_size = ((uint32_t)header[0x0F] << 24) | (header[0x0E] << 16) | (header[0x0D] << 8) | header[0x0C]; - tmd_size = ((uint32_t)header[0x13] << 24) | (header[0x12] << 16) | (header[0x11] << 8) | header[0x10]; - - cert_offset = (CIA_HEADER_SIZE + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; - tik_offset = (cert_offset + cert_size + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; - tmd_offset = (tik_offset + tik_size + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; - content_offset = (tmd_offset + tmd_size + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; - - /* Check if this CIA is encrypted, if it isn't, we can hash it right away */ - - rc_file_seek(file_handle, tmd_offset, SEEK_SET); - if (rc_file_read(file_handle, header, 4) != 4) - return rc_hash_error("Could not read TMD signature type"); - - signature_size = rc_hash_nintendo_3ds_cia_signature_size(header); - if (signature_size == 0) - return 0; /* rc_hash_nintendo_3ds_cia_signature_size will call rc_hash_error, so we don't need to do so here */ - - rc_file_seek(file_handle, signature_size + 0x9E, SEEK_CUR); - if (rc_file_read(file_handle, header, 2) != 2) - return rc_hash_error("Could not read TMD content count"); - - content_count = (header[0] << 8) | header[1]; - - rc_file_seek(file_handle, 0x9C4 - 0x9E - 2, SEEK_CUR); - for (i = 0; i < content_count; i++) - { - if (rc_file_read(file_handle, header, 0x30) != 0x30) - return rc_hash_error("Could not read TMD content chunk"); - - /* Content index 0 is the main content (i.e. the 3DS executable) */ - if (((header[4] << 8) | header[5]) == 0) - break; - - content_offset += ((uint32_t)header[0xC] << 24) | (header[0xD] << 16) | (header[0xE] << 8) | header[0xF]; - } - - if (i == content_count) - return rc_hash_error("Could not find main content chunk in TMD"); - - if ((header[7] & 1) == 0) - { - /* Not encrypted, we can hash the NCCH immediately */ - rc_file_seek(file_handle, content_offset, SEEK_SET); - if (rc_file_read(file_handle, header, 0x200) != 0x200) - return rc_hash_error("Could not read NCCH header"); - - if (memcmp(&header[0x100], "NCCH", 4) != 0) - { - snprintf((char*)header, 0x200, "NCCH header was not at %08X%08X", (unsigned)(content_offset >> 32), (unsigned)content_offset); - return rc_hash_error((const char*)header); - } - - return rc_hash_nintendo_3ds_ncch(md5, file_handle, header, NULL); - } - - if (_3ds_get_cia_normal_key_func == NULL) - return rc_hash_error("An encrypted CIA was detected, but the CIA normal key callback was not set"); - - /* Acquire the encrypted title key, title id, and common key index from the ticket */ - /* These will be needed to decrypt the title key, and that will be needed to decrypt the CIA */ - - rc_file_seek(file_handle, tik_offset, SEEK_SET); - if (rc_file_read(file_handle, header, 4) != 4) - return rc_hash_error("Could not read ticket signature type"); - - signature_size = rc_hash_nintendo_3ds_cia_signature_size(header); - if (signature_size == 0) - return 0; - - rc_file_seek(file_handle, signature_size, SEEK_CUR); - if (rc_file_read(file_handle, header, 0xB2) != 0xB2) - return rc_hash_error("Could not read ticket data"); - - memcpy(title_key, &header[0x7F], sizeof(title_key)); - memcpy(title_id, &header[0x9C], sizeof(title_id)); - common_key_index = header[0xB1]; - - if (common_key_index > 5) - { - snprintf((char*)header, 0x200, "Invalid common key index %02X", (unsigned)common_key_index); - return rc_hash_error((const char*)header); - } - - if (_3ds_get_cia_normal_key_func(common_key_index, normal_key) == 0) - { - snprintf((char*)header, 0x200, "Could not obtain common key %02X", (unsigned)common_key_index); - return rc_hash_error((const char*)header); - } - - memset(iv, 0, sizeof(iv)); - memcpy(iv, title_id, sizeof(title_id)); - AES_init_ctx_iv(&aes, normal_key, iv); - - /* Finally, decrypt the title key */ - AES_CBC_decrypt_buffer(&aes, title_key, sizeof(title_key)); - - /* Now we can hash the NCCH */ - - rc_file_seek(file_handle, content_offset, SEEK_SET); - if (rc_file_read(file_handle, header, 0x200) != 0x200) - return rc_hash_error("Could not read NCCH header"); - - memset(iv, 0, sizeof(iv)); /* Content index is iv (which is always 0 for main content) */ - AES_init_ctx_iv(&aes, title_key, iv); - AES_CBC_decrypt_buffer(&aes, header, 0x200); - - if (memcmp(&header[0x100], "NCCH", 4) != 0) - { - snprintf((char*)header, 0x200, "NCCH header was not at %08X%08X", (unsigned)(content_offset >> 32), (unsigned)content_offset); - return rc_hash_error((const char*)header); - } - - return rc_hash_nintendo_3ds_ncch(md5, file_handle, header, &aes); -} - -static int rc_hash_nintendo_3ds_3dsx(md5_state_t* md5, void* file_handle, uint8_t header[0x200]) -{ - uint8_t* hash_buffer; - uint32_t header_size, reloc_header_size, code_size; - int64_t code_offset; - - header_size = (header[5] << 8) | header[4]; - reloc_header_size = (header[7] << 8) | header[6]; - code_size = ((uint32_t)header[0x13] << 24) | (header[0x12] << 16) | (header[0x11] << 8) | header[0x10]; - - /* 3 relocation headers are in-between the 3DSX header and code segment */ - code_offset = header_size + reloc_header_size * 3; - - if (code_size > MAX_BUFFER_SIZE) - code_size = MAX_BUFFER_SIZE; - - hash_buffer = (uint8_t*)malloc(code_size); - if (!hash_buffer) - { - snprintf((char*)header, 0x200, "Failed to allocate %u bytes", (unsigned)code_size); - return rc_hash_error((const char*)header); - } - - rc_file_seek(file_handle, code_offset, SEEK_SET); - - if (verbose_message_callback) - { - snprintf((char*)header, 0x200, "Hashing %u bytes for 3DSX (at %08X)", (unsigned)code_size, (unsigned)code_offset); - verbose_message_callback((const char*)header); - } - - if (rc_file_read(file_handle, hash_buffer, code_size) != code_size) - { - free(hash_buffer); - return rc_hash_error("Could not read 3DSX code segment"); - } - - md5_append(md5, hash_buffer, code_size); - - free(hash_buffer); - return 1; -} - -static int rc_hash_nintendo_3ds(char hash[33], const char* path) -{ - md5_state_t md5; - void* file_handle; - uint8_t header[0x200]; /* NCCH and NCSD headers are both 0x200 bytes */ - int64_t header_offset; - - file_handle = rc_file_open(path); - if (!file_handle) - return rc_hash_error("Could not open file"); - - rc_file_seek(file_handle, 0, SEEK_SET); - - /* If we don't have a full header, this is probably not a 3DS ROM */ - if (rc_file_read(file_handle, header, sizeof(header)) != sizeof(header)) - { - rc_file_close(file_handle); - return rc_hash_error("Could not read 3DS ROM header"); - } - - md5_init(&md5); - - if (memcmp(&header[0x100], "NCSD", 4) == 0) - { - /* A NCSD container contains 1-8 NCCH partitions */ - /* The first partition (index 0) is reserved for executable content */ - header_offset = ((uint32_t)header[0x123] << 24) | (header[0x122] << 16) | (header[0x121] << 8) | header[0x120]; - /* Offset is in "media units" (1 media unit = 0x200 bytes) */ - header_offset *= 0x200; - - /* We include the NCSD header in the hash, as that will ensure different versions of a game result in a different hash - * This is due to some revisions / languages only ever changing other NCCH paritions (e.g. the game manual) - */ - rc_hash_verbose("Hashing 512 byte NCSD header"); - md5_append(&md5, header, sizeof(header)); - - if (verbose_message_callback) - { - snprintf((char*)header, sizeof(header), "Detected NCSD header, seeking to NCCH partition at %08X%08X", (unsigned)(header_offset >> 32), (unsigned)header_offset); - verbose_message_callback((const char*)header); - } - - rc_file_seek(file_handle, header_offset, SEEK_SET); - if (rc_file_read(file_handle, header, sizeof(header)) != sizeof(header)) - { - rc_file_close(file_handle); - return rc_hash_error("Could not read 3DS NCCH header"); - } - - if (memcmp(&header[0x100], "NCCH", 4) != 0) - { - rc_file_close(file_handle); - snprintf((char*)header, sizeof(header), "3DS NCCH header was not at %08X%08X", (unsigned)(header_offset >> 32), (unsigned)header_offset); - return rc_hash_error((const char*)header); - } - } - - if (memcmp(&header[0x100], "NCCH", 4) == 0) - { - if (rc_hash_nintendo_3ds_ncch(&md5, file_handle, header, NULL)) - { - rc_file_close(file_handle); - return rc_hash_finalize(&md5, hash); - } - - rc_file_close(file_handle); - return rc_hash_error("Failed to hash 3DS NCCH container"); - } - - /* Couldn't identify either an NCSD or NCCH */ - - /* Try to identify this as a CIA */ - if (header[0] == 0x20 && header[1] == 0x20 && header[2] == 0x00 && header[3] == 0x00) - { - rc_hash_verbose("Detected CIA, attempting to find executable NCCH"); - - if (rc_hash_nintendo_3ds_cia(&md5, file_handle, header)) - { - rc_file_close(file_handle); - return rc_hash_finalize(&md5, hash); - } - - rc_file_close(file_handle); - return rc_hash_error("Failed to hash 3DS CIA container"); - } - - /* This might be a homebrew game, try to detect that */ - if (memcmp(&header[0], "3DSX", 4) == 0) - { - rc_hash_verbose("Detected 3DSX"); - - if (rc_hash_nintendo_3ds_3dsx(&md5, file_handle, header)) - { - rc_file_close(file_handle); - return rc_hash_finalize(&md5, hash); - } - - rc_file_close(file_handle); - return rc_hash_error("Failed to hash 3DS 3DSX container"); - } - - /* Raw ELF marker (AXF/ELF files) */ - if (memcmp(&header[0], "\x7f\x45\x4c\x46", 4) == 0) - { - rc_hash_verbose("Detected AXF/ELF file, hashing entire file"); - - /* Don't bother doing anything fancy here, just hash entire file */ - rc_file_close(file_handle); - return rc_hash_whole_file(hash, path); - } - - rc_file_close(file_handle); - return rc_hash_error("Not a 3DS ROM"); -} - -static int rc_hash_nintendo_ds(char hash[33], const char* path) -{ - uint8_t header[512]; - uint8_t* hash_buffer; - uint32_t hash_size, arm9_size, arm9_addr, arm7_size, arm7_addr, icon_addr; - size_t num_read; - int64_t offset = 0; - md5_state_t md5; - void* file_handle; - - file_handle = rc_file_open(path); - if (!file_handle) - return rc_hash_error("Could not open file"); - - rc_file_seek(file_handle, 0, SEEK_SET); - if (rc_file_read(file_handle, header, sizeof(header)) != 512) - return rc_hash_error("Failed to read header"); - - if (header[0] == 0x2E && header[1] == 0x00 && header[2] == 0x00 && header[3] == 0xEA && - header[0xB0] == 0x44 && header[0xB1] == 0x46 && header[0xB2] == 0x96 && header[0xB3] == 0) - { - /* SuperCard header detected, ignore it */ - rc_hash_verbose("Ignoring SuperCard header"); - - offset = 512; - rc_file_seek(file_handle, offset, SEEK_SET); - rc_file_read(file_handle, header, sizeof(header)); - } - - arm9_addr = header[0x20] | (header[0x21] << 8) | (header[0x22] << 16) | (header[0x23] << 24); - arm9_size = header[0x2C] | (header[0x2D] << 8) | (header[0x2E] << 16) | (header[0x2F] << 24); - arm7_addr = header[0x30] | (header[0x31] << 8) | (header[0x32] << 16) | (header[0x33] << 24); - arm7_size = header[0x3C] | (header[0x3D] << 8) | (header[0x3E] << 16) | (header[0x3F] << 24); - icon_addr = header[0x68] | (header[0x69] << 8) | (header[0x6A] << 16) | (header[0x6B] << 24); - - if (arm9_size + arm7_size > 16 * 1024 * 1024) - { - /* sanity check - code blocks are typically less than 1MB each - assume not a DS ROM */ - snprintf((char*)header, sizeof(header), "arm9 code size (%u) + arm7 code size (%u) exceeds 16MB", arm9_size, arm7_size); - return rc_hash_error((const char*)header); - } - - hash_size = 0xA00; - if (arm9_size > hash_size) - hash_size = arm9_size; - if (arm7_size > hash_size) - hash_size = arm7_size; - - hash_buffer = (uint8_t*)malloc(hash_size); - if (!hash_buffer) - { - rc_file_close(file_handle); - - snprintf((char*)header, sizeof(header), "Failed to allocate %u bytes", hash_size); - return rc_hash_error((const char*)header); - } - - md5_init(&md5); - - rc_hash_verbose("Hashing 352 byte header"); - md5_append(&md5, header, 0x160); - - if (verbose_message_callback) - { - snprintf((char*)header, sizeof(header), "Hashing %u byte arm9 code (at %08X)", arm9_size, arm9_addr); - verbose_message_callback((const char*)header); - } - - rc_file_seek(file_handle, arm9_addr + offset, SEEK_SET); - rc_file_read(file_handle, hash_buffer, arm9_size); - md5_append(&md5, hash_buffer, arm9_size); - - if (verbose_message_callback) - { - snprintf((char*)header, sizeof(header), "Hashing %u byte arm7 code (at %08X)", arm7_size, arm7_addr); - verbose_message_callback((const char*)header); - } - - rc_file_seek(file_handle, arm7_addr + offset, SEEK_SET); - rc_file_read(file_handle, hash_buffer, arm7_size); - md5_append(&md5, hash_buffer, arm7_size); - - if (verbose_message_callback) - { - snprintf((char*)header, sizeof(header), "Hashing 2560 byte icon and labels data (at %08X)", icon_addr); - verbose_message_callback((const char*)header); - } - - rc_file_seek(file_handle, icon_addr + offset, SEEK_SET); - num_read = rc_file_read(file_handle, hash_buffer, 0xA00); - if (num_read < 0xA00) - { - /* some homebrew games don't provide a full icon block, and no data after the icon block. - * if we didn't get a full icon block, fill the remaining portion with 0s - */ - if (verbose_message_callback) - { - snprintf((char*)header, sizeof(header), "Warning: only got %u bytes for icon and labels data, 0-padding to 2560 bytes", (unsigned)num_read); - verbose_message_callback((const char*)header); - } - - memset(&hash_buffer[num_read], 0, 0xA00 - num_read); - } - md5_append(&md5, hash_buffer, 0xA00); - - free(hash_buffer); - rc_file_close(file_handle); - - 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) */ - uint32_t calc_size = ((uint32_t)buffer_size / 0x20000) * 0x20000; - if (buffer_size - calc_size == 512) - { - rc_hash_verbose("Ignoring PCE header"); - - buffer += 512; - buffer_size -= 512; - } - - return rc_hash_buffer(hash, buffer, buffer_size); -} - -static int rc_hash_pce_track(char hash[33], void* track_handle) -{ - uint8_t buffer[2048]; - md5_state_t md5; - uint32_t sector, num_sectors; - uint32_t size; - - /* the PC-Engine uses the second sector to specify boot information and program name. - * the string "PC Engine CD-ROM SYSTEM" should exist at 32 bytes into the sector - * http://shu.sheldows.com/shu/download/pcedocs/pce_cdrom.html - */ - if (rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle) + 1, buffer, 128) < 128) - { - return rc_hash_error("Not a PC Engine CD"); - } - - /* normal PC Engine CD will have a header block in sector 1 */ - if (memcmp("PC Engine CD-ROM SYSTEM", &buffer[32], 23) == 0) - { - /* the title of the disc is the last 22 bytes of the header */ - md5_init(&md5); - md5_append(&md5, &buffer[106], 22); - - if (verbose_message_callback) - { - char message[128]; - buffer[128] = '\0'; - snprintf(message, sizeof(message), "Found PC Engine CD, title=%.22s", &buffer[106]); - verbose_message_callback(message); - } - - /* the first three bytes specify the sector of the program data, and the fourth byte - * is the number of sectors. - */ - sector = (buffer[0] << 16) + (buffer[1] << 8) + buffer[2]; - num_sectors = buffer[3]; - - if (verbose_message_callback) - { - char message[128]; - snprintf(message, sizeof(message), "Hashing %d sectors starting at sector %d", num_sectors, sector); - verbose_message_callback(message); - } - - sector += rc_cd_first_track_sector(track_handle); - while (num_sectors > 0) - { - rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); - md5_append(&md5, buffer, sizeof(buffer)); - - ++sector; - --num_sectors; - } - } - /* GameExpress CDs use a standard Joliet filesystem - locate and hash the BOOT.BIN */ - else if ((sector = rc_cd_find_file_sector(track_handle, "BOOT.BIN", &size)) != 0 && size < MAX_BUFFER_SIZE) - { - md5_init(&md5); - while (size > sizeof(buffer)) - { - rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); - md5_append(&md5, buffer, sizeof(buffer)); - - ++sector; - size -= sizeof(buffer); - } - - if (size > 0) - { - rc_cd_read_sector(track_handle, sector, buffer, size); - md5_append(&md5, buffer, size); - } - } - else - { - return rc_hash_error("Not a PC Engine CD"); - } - - return rc_hash_finalize(&md5, hash); -} - -static int rc_hash_pce_cd(char hash[33], const char* path) -{ - int result; - void* track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_FIRST_DATA); - if (!track_handle) - return rc_hash_error("Could not open track"); - - result = rc_hash_pce_track(hash, track_handle); - - rc_cd_close_track(track_handle); - - return result; -} - -static int rc_hash_pcfx_cd(char hash[33], const char* path) -{ - uint8_t buffer[2048]; - void* track_handle; - md5_state_t md5; - int sector, num_sectors; - - /* PC-FX executable can be in any track. Assume it's in the largest data track and check there first */ - track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_LARGEST); - if (!track_handle) - return rc_hash_error("Could not open track"); - - /* PC-FX CD will have a header marker in sector 0 */ - sector = rc_cd_first_track_sector(track_handle); - rc_cd_read_sector(track_handle, sector, buffer, 32); - if (memcmp("PC-FX:Hu_CD-ROM", &buffer[0], 15) != 0) - { - rc_cd_close_track(track_handle); - - /* not found in the largest data track, check track 2 */ - track_handle = rc_cd_open_track(path, 2); - if (!track_handle) - return rc_hash_error("Could not open track"); - - sector = rc_cd_first_track_sector(track_handle); - rc_cd_read_sector(track_handle, sector, buffer, 32); - } - - if (memcmp("PC-FX:Hu_CD-ROM", &buffer[0], 15) == 0) - { - /* PC-FX boot header fills the first two sectors of the disc - * https://bitbucket.org/trap15/pcfxtools/src/master/pcfx-cdlink.c - * the important stuff is the first 128 bytes of the second sector (title being the first 32) */ - rc_cd_read_sector(track_handle, sector + 1, buffer, 128); - - md5_init(&md5); - md5_append(&md5, buffer, 128); - - if (verbose_message_callback) - { - char message[128]; - buffer[128] = '\0'; - snprintf(message, sizeof(message), "Found PC-FX CD, title=%.32s", &buffer[0]); - verbose_message_callback(message); - } - - /* the program sector is in bytes 33-36 (assume byte 36 is 0) */ - sector = (buffer[34] << 16) + (buffer[33] << 8) + buffer[32]; - - /* the number of sectors the program occupies is in bytes 37-40 (assume byte 40 is 0) */ - num_sectors = (buffer[38] << 16) + (buffer[37] << 8) + buffer[36]; - - if (verbose_message_callback) - { - char message[128]; - snprintf(message, sizeof(message), "Hashing %d sectors starting at sector %d", num_sectors, sector); - verbose_message_callback(message); - } - - sector += rc_cd_first_track_sector(track_handle); - while (num_sectors > 0) - { - rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)); - md5_append(&md5, buffer, sizeof(buffer)); - - ++sector; - --num_sectors; - } - } - else - { - int result = 0; - rc_cd_read_sector(track_handle, sector + 1, buffer, 128); - - /* some PC-FX CDs still identify as PCE CDs */ - if (memcmp("PC Engine CD-ROM SYSTEM", &buffer[32], 23) == 0) - result = rc_hash_pce_track(hash, track_handle); - - rc_cd_close_track(track_handle); - if (result) - return result; - - return rc_hash_error("Not a PC-FX CD"); - } - - rc_cd_close_track(track_handle); - - return rc_hash_finalize(&md5, hash); -} - -static int rc_hash_dreamcast(char hash[33], const char* path) -{ - uint8_t buffer[256] = ""; - void* track_handle; - char exe_file[32] = ""; - uint32_t size; - uint32_t sector; - int result = 0; - md5_state_t md5; - int i = 0; - - /* track 03 is the data track that contains the TOC and IP.BIN */ - track_handle = rc_cd_open_track(path, 3); - if (track_handle) - { - /* first 256 bytes from first sector should have IP.BIN structure that stores game meta information - * https://mc.pp.se/dc/ip.bin.html */ - rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle), buffer, sizeof(buffer)); - } - - if (memcmp(&buffer[0], "SEGA SEGAKATANA ", 16) != 0) - { - if (track_handle) - rc_cd_close_track(track_handle); - - /* not a gd-rom dreamcast file. check for mil-cd by looking for the marker in the first data track */ - track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_FIRST_DATA); - if (!track_handle) - return rc_hash_error("Could not open track"); - - rc_cd_read_sector(track_handle, rc_cd_first_track_sector(track_handle), buffer, sizeof(buffer)); - if (memcmp(&buffer[0], "SEGA SEGAKATANA ", 16) != 0) - { - /* did not find marker on track 3 or first data track */ - rc_cd_close_track(track_handle); - return rc_hash_error("Not a Dreamcast CD"); - } - } - - /* start the hash with the game meta information */ - md5_init(&md5); - md5_append(&md5, (md5_byte_t*)buffer, 256); - - if (verbose_message_callback) - { - char message[256]; - uint8_t* ptr = &buffer[0xFF]; - while (ptr > &buffer[0x80] && ptr[-1] == ' ') - --ptr; - *ptr = '\0'; - - snprintf(message, sizeof(message), "Found Dreamcast CD: %.128s (%.16s)", (const char*)&buffer[0x80], (const char*)&buffer[0x40]); - verbose_message_callback(message); - } - - /* the boot filename is 96 bytes into the meta information (https://mc.pp.se/dc/ip0000.bin.html) */ - /* remove whitespace from bootfile */ - i = 0; - while (!isspace((unsigned char)buffer[96 + i]) && i < 16) - ++i; - - /* sometimes boot file isn't present on meta information. - * nothing can be done, as even the core doesn't run the game in this case. */ - if (i == 0) - { - rc_cd_close_track(track_handle); - return rc_hash_error("Boot executable not specified on IP.BIN"); - } - - memcpy(exe_file, &buffer[96], i); - exe_file[i] = '\0'; - - sector = rc_cd_find_file_sector(track_handle, exe_file, &size); - if (sector == 0) - { - rc_cd_close_track(track_handle); - return rc_hash_error("Could not locate boot executable"); - } - - if (rc_cd_read_sector(track_handle, sector, buffer, 1)) - { - /* the boot executable is in the primary data track */ - } - else - { - rc_cd_close_track(track_handle); - - /* the boot executable is normally in the last track */ - track_handle = rc_cd_open_track(path, RC_HASH_CDTRACK_LAST); - } - - result = rc_hash_cd_file(&md5, track_handle, sector, NULL, size, "boot executable"); - rc_cd_close_track(track_handle); - - rc_hash_finalize(&md5, hash); - return result; -} - -static int rc_hash_find_playstation_executable(void* track_handle, const char* boot_key, const char* cdrom_prefix, - char exe_name[], uint32_t exe_name_size, uint32_t* exe_size) -{ - uint8_t buffer[2048]; - uint32_t size; - char* ptr; - char* start; - const size_t boot_key_len = strlen(boot_key); - const size_t cdrom_prefix_len = strlen(cdrom_prefix); - int sector; - - sector = rc_cd_find_file_sector(track_handle, "SYSTEM.CNF", NULL); - if (!sector) - return 0; - - size = (uint32_t)rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer) - 1); - buffer[size] = '\0'; - - sector = 0; - for (ptr = (char*)buffer; *ptr; ++ptr) - { - if (strncmp(ptr, boot_key, boot_key_len) == 0) - { - ptr += boot_key_len; - while (isspace((unsigned char)*ptr)) - ++ptr; - - if (*ptr == '=') - { - ++ptr; - while (isspace((unsigned char)*ptr)) - ++ptr; - - if (strncmp(ptr, cdrom_prefix, cdrom_prefix_len) == 0) - ptr += cdrom_prefix_len; - while (*ptr == '\\') - ++ptr; - - start = ptr; - while (!isspace((unsigned char)*ptr) && *ptr != ';') - ++ptr; - - size = (uint32_t)(ptr - start); - if (size >= exe_name_size) - size = exe_name_size - 1; - - memcpy(exe_name, start, size); - exe_name[size] = '\0'; - - if (verbose_message_callback) - { - snprintf((char*)buffer, sizeof(buffer), "Looking for boot executable: %s", exe_name); - verbose_message_callback((const char*)buffer); - } - - sector = rc_cd_find_file_sector(track_handle, exe_name, exe_size); - break; - } - } - - /* advance to end of line */ - while (*ptr && *ptr != '\n') - ++ptr; - } - - return sector; -} - -static int rc_hash_psx(char hash[33], const char* path) -{ - uint8_t buffer[32]; - char exe_name[64] = ""; - void* track_handle; - uint32_t sector; - uint32_t size; - int result = 0; - md5_state_t md5; - - track_handle = rc_cd_open_track(path, 1); - if (!track_handle) - return rc_hash_error("Could not open track"); - - sector = rc_hash_find_playstation_executable(track_handle, "BOOT", "cdrom:", exe_name, sizeof(exe_name), &size); - if (!sector) - { - sector = rc_cd_find_file_sector(track_handle, "PSX.EXE", &size); - if (sector) - memcpy(exe_name, "PSX.EXE", 8); - } - - if (!sector) - { - rc_hash_error("Could not locate primary executable"); - } - else if (rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)) < sizeof(buffer)) - { - rc_hash_error("Could not read primary executable"); - } - else - { - if (memcmp(buffer, "PS-X EXE", 7) != 0) - { - if (verbose_message_callback) - { - char message[128]; - snprintf(message, sizeof(message), "%s did not contain PS-X EXE marker", exe_name); - verbose_message_callback(message); - } - } - else - { - /* the PS-X EXE header specifies the executable size as a 4-byte value 28 bytes into the header, which doesn't - * include the header itself. We want to include the header in the hash, so append another 2048 to that value. - */ - size = (((uint8_t)buffer[31] << 24) | ((uint8_t)buffer[30] << 16) | ((uint8_t)buffer[29] << 8) | (uint8_t)buffer[28]) + 2048; - } - - /* there's a few games that use a singular engine and only differ via their data files. luckily, they have unique - * serial numbers, and use the serial number as the boot file in the standard way. include the boot file name in the hash. - */ - md5_init(&md5); - md5_append(&md5, (md5_byte_t*)exe_name, (int)strlen(exe_name)); - - result = rc_hash_cd_file(&md5, track_handle, sector, exe_name, size, "primary executable"); - rc_hash_finalize(&md5, hash); - } - - rc_cd_close_track(track_handle); - - return result; -} - -static int rc_hash_ps2(char hash[33], const char* path) -{ - uint8_t buffer[4]; - char exe_name[64] = ""; - void* track_handle; - uint32_t sector; - uint32_t size; - int result = 0; - md5_state_t md5; - - track_handle = rc_cd_open_track(path, 1); - if (!track_handle) - return rc_hash_error("Could not open track"); - - sector = rc_hash_find_playstation_executable(track_handle, "BOOT2", "cdrom0:", exe_name, sizeof(exe_name), &size); - if (!sector) - { - rc_hash_error("Could not locate primary executable"); - } - else if (rc_cd_read_sector(track_handle, sector, buffer, sizeof(buffer)) < sizeof(buffer)) - { - rc_hash_error("Could not read primary executable"); - } - else - { - if (memcmp(buffer, "\x7f\x45\x4c\x46", 4) != 0) - { - if (verbose_message_callback) - { - char message[128]; - snprintf(message, sizeof(message), "%s did not contain ELF marker", exe_name); - verbose_message_callback(message); - } - } - - /* there's a few games that use a singular engine and only differ via their data files. luckily, they have unique - * serial numbers, and use the serial number as the boot file in the standard way. include the boot file name in the hash. - */ - md5_init(&md5); - md5_append(&md5, (md5_byte_t*)exe_name, (int)strlen(exe_name)); - - result = rc_hash_cd_file(&md5, track_handle, sector, exe_name, size, "primary executable"); - rc_hash_finalize(&md5, hash); - } - - rc_cd_close_track(track_handle); - - return result; -} - -static int rc_hash_psp(char hash[33], const char* path) -{ - void* track_handle; - uint32_t sector; - uint32_t size; - md5_state_t md5; - - /* https://www.psdevwiki.com/psp/PBP - * A PBP file is an archive containing the PARAM.SFO, primary executable, and a bunch of metadata. - * While we could extract the PARAM.SFO and primary executable to mimic the normal PSP hashing logic, - * it's easier to just hash the entire file. This also helps alleviate issues where the primary - * executable is just a game engine and the only differentiating data would be the metadata. */ - if (rc_path_compare_extension(path, "pbp")) - return rc_hash_whole_file(hash, path); - - track_handle = rc_cd_open_track(path, 1); - if (!track_handle) - return rc_hash_error("Could not open track"); - - /* http://www.romhacking.net/forum/index.php?topic=30899.0 - * PSP_GAME/PARAM.SFO contains key/value pairs identifying the game for the system (i.e. serial number, - * name, version). PSP_GAME/SYSDIR/EBOOT.BIN is the encrypted primary executable. - */ - 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); -} - -static int rc_hash_sega_cd(char hash[33], const char* path) -{ - uint8_t buffer[512]; - void* track_handle; - - track_handle = rc_cd_open_track(path, 1); - if (!track_handle) - return rc_hash_error("Could not open track"); - - /* the first 512 bytes of sector 0 are a volume header and ROM header that uniquely identify the game. - * After that is an arbitrary amount of code that ensures the game is being run in the correct region. - * Then more arbitrary code follows that actually starts the boot process. Somewhere in there, the - * primary executable is loaded. In many cases, a single game will have multiple executables, so even - * if we could determine the primary one, it's just the tip of the iceberg. As such, we've decided that - * hashing the volume and ROM headers is sufficient for identifying the game, and we'll have to trust - * that our players aren't modifying anything else on the disc. - */ - rc_cd_read_sector(track_handle, 0, buffer, sizeof(buffer)); - rc_cd_close_track(track_handle); - - if (memcmp(buffer, "SEGADISCSYSTEM ", 16) != 0 && /* Sega CD */ - memcmp(buffer, "SEGA SEGASATURN ", 16) != 0) /* Sega Saturn */ - { - return rc_hash_error("Not a Sega CD"); - } - - return rc_hash_buffer(hash, buffer, sizeof(buffer)); -} - -static int rc_hash_scv(char hash[33], const uint8_t* buffer, size_t buffer_size) -{ - /* if the file contains a header, ignore it */ - /* https://gitlab.com/MaaaX-EmuSCV/libretro-emuscv/-/blob/master/readme.txt#L211 */ - if (memcmp(buffer, "EmuSCV", 6) == 0) - { - rc_hash_verbose("Ignoring SCV header"); - - buffer += 32; - buffer_size -= 32; - } - - return rc_hash_buffer(hash, buffer, buffer_size); -} - -static int rc_hash_snes(char hash[33], const uint8_t* buffer, size_t buffer_size) -{ - /* if the file contains a header, ignore it */ - uint32_t calc_size = ((uint32_t)buffer_size / 0x2000) * 0x2000; - if (buffer_size - calc_size == 512) - { - rc_hash_verbose("Ignoring SNES header"); - - buffer += 512; - buffer_size -= 512; - } - - return rc_hash_buffer(hash, buffer, buffer_size); + return rc_hash_finalize(iterator, &md5, hash); } struct rc_buffered_file @@ -3022,11 +435,10 @@ static void* rc_file_open_buffered_file(const char* path) return handle; } -void rc_file_seek_buffered_file(void* file_handle, int64_t offset, int origin) +static void rc_file_seek_buffered_file(void* file_handle, int64_t offset, int origin) { struct rc_buffered_file* buffered_file = (struct rc_buffered_file*)file_handle; - switch (origin) - { + switch (origin) { case SEEK_SET: buffered_file->read_ptr = buffered_file->data + offset; break; case SEEK_CUR: buffered_file->read_ptr += offset; break; case SEEK_END: buffered_file->read_ptr = buffered_file->data + buffered_file->data_size + offset; break; @@ -3038,13 +450,13 @@ void rc_file_seek_buffered_file(void* file_handle, int64_t offset, int origin) buffered_file->read_ptr = buffered_file->data + buffered_file->data_size; } -int64_t rc_file_tell_buffered_file(void* file_handle) +static int64_t rc_file_tell_buffered_file(void* file_handle) { struct rc_buffered_file* buffered_file = (struct rc_buffered_file*)file_handle; return (buffered_file->read_ptr - buffered_file->data); } -size_t rc_file_read_buffered_file(void* file_handle, void* buffer, size_t requested_bytes) +static size_t rc_file_read_buffered_file(void* file_handle, void* buffer, size_t requested_bytes) { struct rc_buffered_file* buffered_file = (struct rc_buffered_file*)file_handle; const int64_t remaining = buffered_file->data_size - (buffered_file->read_ptr - buffered_file->data); @@ -3056,44 +468,41 @@ size_t rc_file_read_buffered_file(void* file_handle, void* buffer, size_t reques return requested_bytes; } -void rc_file_close_buffered_file(void* file_handle) +static void rc_file_close_buffered_file(void* file_handle) { free(file_handle); } -static int rc_hash_file_from_buffer(char hash[33], uint32_t console_id, const uint8_t* buffer, size_t buffer_size) +static int rc_hash_file_from_buffer(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator) { - struct rc_hash_filereader buffered_filereader_funcs; - struct rc_hash_filereader* old_filereader = filereader; int result; - memset(&buffered_filereader_funcs, 0, sizeof(buffered_filereader_funcs)); - buffered_filereader_funcs.open = rc_file_open_buffered_file; - buffered_filereader_funcs.close = rc_file_close_buffered_file; - buffered_filereader_funcs.read = rc_file_read_buffered_file; - buffered_filereader_funcs.seek = rc_file_seek_buffered_file; - buffered_filereader_funcs.tell = rc_file_tell_buffered_file; - filereader = &buffered_filereader_funcs; + rc_hash_iterator_t buffered_file_iterator; + memset(&buffered_file_iterator, 0, sizeof(buffered_file_iterator)); + memcpy(&buffered_file_iterator.callbacks, &iterator->callbacks, sizeof(iterator->callbacks)); - rc_buffered_file.data = rc_buffered_file.read_ptr = buffer; - rc_buffered_file.data_size = buffer_size; + buffered_file_iterator.callbacks.filereader.open = rc_file_open_buffered_file; + buffered_file_iterator.callbacks.filereader.close = rc_file_close_buffered_file; + buffered_file_iterator.callbacks.filereader.read = rc_file_read_buffered_file; + buffered_file_iterator.callbacks.filereader.seek = rc_file_seek_buffered_file; + buffered_file_iterator.callbacks.filereader.tell = rc_file_tell_buffered_file; + buffered_file_iterator.path = "memory stream"; - result = rc_hash_generate_from_file(hash, console_id, "[buffered file]"); + rc_buffered_file.data = rc_buffered_file.read_ptr = iterator->buffer; + rc_buffered_file.data_size = iterator->buffer_size; - filereader = old_filereader; + result = rc_hash_from_file(hash, console_id, &buffered_file_iterator); + + buffered_file_iterator.path = NULL; + rc_hash_destroy_iterator(&buffered_file_iterator); return result; } -int rc_hash_generate_from_buffer(char hash[33], uint32_t console_id, const uint8_t* buffer, size_t buffer_size) +static int rc_hash_from_buffer(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator) { - switch (console_id) - { + switch (console_id) { default: - { - char message[128]; - snprintf(message, sizeof(message), "Unsupported console for buffer hash: %d", console_id); - return rc_hash_error(message); - } + return rc_hash_iterator_error_formatted(iterator, "Unsupported console for buffer hash: %d", console_id); case RC_CONSOLE_AMSTRAD_PC: case RC_CONSOLE_APPLE_II: @@ -3130,39 +539,41 @@ int rc_hash_generate_from_buffer(char hash[33], uint32_t console_id, const uint8 case RC_CONSOLE_WASM4: case RC_CONSOLE_WONDERSWAN: case RC_CONSOLE_ZX_SPECTRUM: - return rc_hash_buffer(hash, buffer, buffer_size); + return rc_hash_buffer(hash, iterator->buffer, iterator->buffer_size, iterator); +#ifndef RC_HASH_NO_ROM case RC_CONSOLE_ARDUBOY: - /* https://en.wikipedia.org/wiki/Intel_HEX */ - return rc_hash_text(hash, buffer, buffer_size); + return rc_hash_arduboy(hash, iterator); case RC_CONSOLE_ATARI_7800: - return rc_hash_7800(hash, buffer, buffer_size); + return rc_hash_7800(hash, iterator); case RC_CONSOLE_ATARI_LYNX: - return rc_hash_lynx(hash, buffer, buffer_size); + return rc_hash_lynx(hash, iterator); + case RC_CONSOLE_FAMICOM_DISK_SYSTEM: case RC_CONSOLE_NINTENDO: - return rc_hash_nes(hash, buffer, buffer_size); + return rc_hash_nes(hash, iterator); case RC_CONSOLE_PC_ENGINE: /* NOTE: does not support PCEngine CD */ - return rc_hash_pce(hash, buffer, buffer_size); + return rc_hash_pce(hash, iterator); case RC_CONSOLE_SUPER_CASSETTEVISION: - return rc_hash_scv(hash, buffer, buffer_size); + return rc_hash_scv(hash, iterator); case RC_CONSOLE_SUPER_NINTENDO: - return rc_hash_snes(hash, buffer, buffer_size); + return rc_hash_snes(hash, iterator); +#endif case RC_CONSOLE_NINTENDO_64: case RC_CONSOLE_NINTENDO_3DS: case RC_CONSOLE_NINTENDO_DS: case RC_CONSOLE_NINTENDO_DSI: - return rc_hash_file_from_buffer(hash, console_id, buffer, buffer_size); + return rc_hash_file_from_buffer(hash, console_id, iterator); } } -static int rc_hash_whole_file(char hash[33], const char* path) +int rc_hash_whole_file(char hash[33], const rc_hash_iterator_t* iterator) { md5_state_t md5; uint8_t* buffer; @@ -3172,94 +583,86 @@ static int rc_hash_whole_file(char hash[33], const char* path) size_t remaining; int result = 0; - file_handle = rc_file_open(path); + file_handle = rc_file_open(iterator, iterator->path); if (!file_handle) - return rc_hash_error("Could not open file"); + return rc_hash_iterator_error(iterator, "Could not open file"); - rc_file_seek(file_handle, 0, SEEK_END); - size = rc_file_tell(file_handle); + rc_file_seek(iterator, file_handle, 0, SEEK_END); + size = rc_file_tell(iterator, file_handle); - if (verbose_message_callback) - { - char message[1024]; - if (size > MAX_BUFFER_SIZE) - snprintf(message, sizeof(message), "Hashing first %u bytes (of %u bytes) of %s", MAX_BUFFER_SIZE, (unsigned)size, rc_path_get_filename(path)); - else - snprintf(message, sizeof(message), "Hashing %s (%u bytes)", rc_path_get_filename(path), (unsigned)size); - verbose_message_callback(message); - } - - if (size > MAX_BUFFER_SIZE) + if (size > MAX_BUFFER_SIZE) { + rc_hash_iterator_verbose_formatted(iterator, "Hashing first %u bytes (of %u bytes) of %s", MAX_BUFFER_SIZE, (unsigned)size, rc_path_get_filename(iterator->path)); remaining = MAX_BUFFER_SIZE; - else + } + else { + rc_hash_iterator_verbose_formatted(iterator, "Hashing %s (%u bytes)", rc_path_get_filename(iterator->path), (unsigned)size); remaining = (size_t)size; + } md5_init(&md5); buffer = (uint8_t*)malloc(buffer_size); - if (buffer) - { - rc_file_seek(file_handle, 0, SEEK_SET); - while (remaining >= buffer_size) - { - rc_file_read(file_handle, buffer, (int)buffer_size); + if (buffer) { + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + while (remaining >= buffer_size) { + rc_file_read(iterator, file_handle, buffer, (int)buffer_size); md5_append(&md5, buffer, (int)buffer_size); remaining -= buffer_size; } - if (remaining > 0) - { - rc_file_read(file_handle, buffer, (int)remaining); + if (remaining > 0) { + rc_file_read(iterator, file_handle, buffer, (int)remaining); md5_append(&md5, buffer, (int)remaining); } free(buffer); - result = rc_hash_finalize(&md5, hash); + result = rc_hash_finalize(iterator, &md5, hash); } - rc_file_close(file_handle); + rc_file_close(iterator, file_handle); return result; } -static int rc_hash_buffered_file(char hash[33], uint32_t console_id, const char* path) +int rc_hash_buffered_file(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator) { uint8_t* buffer; int64_t size; int result = 0; void* file_handle; - file_handle = rc_file_open(path); + file_handle = rc_file_open(iterator, iterator->path); if (!file_handle) - return rc_hash_error("Could not open file"); + return rc_hash_iterator_error(iterator, "Could not open file"); - rc_file_seek(file_handle, 0, SEEK_END); - size = rc_file_tell(file_handle); + rc_file_seek(iterator, file_handle, 0, SEEK_END); + size = rc_file_tell(iterator, file_handle); - if (verbose_message_callback) - { - char message[1024]; - if (size > MAX_BUFFER_SIZE) - snprintf(message, sizeof(message), "Buffering first %u bytes (of %d bytes) of %s", MAX_BUFFER_SIZE, (unsigned)size, rc_path_get_filename(path)); - else - snprintf(message, sizeof(message), "Buffering %s (%d bytes)", rc_path_get_filename(path), (unsigned)size); - verbose_message_callback(message); + if (size > MAX_BUFFER_SIZE) { + rc_hash_iterator_verbose_formatted(iterator, "Buffering first %u bytes (of %d bytes) of %s", MAX_BUFFER_SIZE, (unsigned)size, rc_path_get_filename(iterator->path)); + size = MAX_BUFFER_SIZE; + } + else { + rc_hash_iterator_verbose_formatted(iterator, "Buffering %s (%d bytes)", rc_path_get_filename(iterator->path), (unsigned)size); } - if (size > MAX_BUFFER_SIZE) - size = MAX_BUFFER_SIZE; - buffer = (uint8_t*)malloc((size_t)size); - if (buffer) - { - rc_file_seek(file_handle, 0, SEEK_SET); - rc_file_read(file_handle, buffer, (int)size); + if (buffer) { + rc_hash_iterator_t buffer_iterator; + memset(&buffer_iterator, 0, sizeof(buffer_iterator)); + memcpy(&buffer_iterator.callbacks, &iterator->callbacks, sizeof(iterator->callbacks)); + buffer_iterator.path = iterator->path; + buffer_iterator.buffer = buffer; + buffer_iterator.buffer_size = (size_t)size; - result = rc_hash_generate_from_buffer(hash, console_id, buffer, (size_t)size); + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, (int)size); + + result = rc_hash_from_buffer(hash, console_id, &buffer_iterator); free(buffer); } - rc_file_close(file_handle); + rc_file_close(iterator, file_handle); return result; } @@ -3277,8 +680,7 @@ static int rc_hash_path_is_absolute(const char* path) return 1; /* "scheme:/path/to/file" */ - while (*path) - { + while (*path) { if (path[0] == ':' && path[1] == '/') return 1; @@ -3288,32 +690,28 @@ static int rc_hash_path_is_absolute(const char* path) return 0; } -static const char* rc_hash_get_first_item_from_playlist(const char* path) -{ +static const char* rc_hash_get_first_item_from_playlist(const rc_hash_iterator_t* iterator) { char buffer[1024]; char* disc_path; char* ptr, *start, *next; size_t num_read, path_len, file_len; void* file_handle; - file_handle = rc_file_open(path); - if (!file_handle) - { - rc_hash_error("Could not open playlist"); + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) { + rc_hash_iterator_error(iterator, "Could not open playlist"); return NULL; } - num_read = rc_file_read(file_handle, buffer, sizeof(buffer) - 1); + num_read = rc_file_read(iterator, file_handle, buffer, sizeof(buffer) - 1); buffer[num_read] = '\0'; - rc_file_close(file_handle); + rc_file_close(iterator, file_handle); ptr = start = buffer; - do - { + do { /* ignore empty and commented lines */ - while (*ptr == '#' || *ptr == '\r' || *ptr == '\n') - { + while (*ptr == '#' || *ptr == '\r' || *ptr == '\n') { while (*ptr && *ptr != '\n') ++ptr; if (*ptr) @@ -3343,62 +741,53 @@ static const char* rc_hash_get_first_item_from_playlist(const char* path) ptr = next + 1; } while (1); - if (verbose_message_callback) - { - char message[1024]; - snprintf(message, sizeof(message), "Extracted %.*s from playlist", (int)file_len, start); - verbose_message_callback(message); - } + rc_hash_iterator_verbose_formatted(iterator, "Extracted %.*s from playlist", (int)file_len, start); start[file_len++] = '\0'; if (rc_hash_path_is_absolute(start)) path_len = 0; else - path_len = rc_path_get_filename(path) - path; + path_len = rc_path_get_filename(iterator->path) - iterator->path; disc_path = (char*)malloc(path_len + file_len + 1); if (!disc_path) return NULL; if (path_len) - memcpy(disc_path, path, path_len); + memcpy(disc_path, iterator->path, path_len); memcpy(&disc_path[path_len], start, file_len); return disc_path; } -static int rc_hash_generate_from_playlist(char hash[33], uint32_t console_id, const char* path) -{ - int result; +static int rc_hash_generate_from_playlist(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator) { + rc_hash_iterator_t first_file_iterator; const char* disc_path; + int result; - if (verbose_message_callback) - { - char message[1024]; - snprintf(message, sizeof(message), "Processing playlist: %s", rc_path_get_filename(path)); - verbose_message_callback(message); - } + rc_hash_iterator_verbose_formatted(iterator, "Processing playlist: %s", rc_path_get_filename(iterator->path)); - disc_path = rc_hash_get_first_item_from_playlist(path); + disc_path = rc_hash_get_first_item_from_playlist(iterator); if (!disc_path) - return rc_hash_error("Failed to get first item from playlist"); + return rc_hash_iterator_error(iterator, "Failed to get first item from playlist"); - result = rc_hash_generate_from_file(hash, console_id, disc_path); + memset(&first_file_iterator, 0, sizeof(first_file_iterator)); + memcpy(&first_file_iterator.callbacks, &iterator->callbacks, sizeof(iterator->callbacks)); + first_file_iterator.path = disc_path; /* rc_hash_destory_iterator will free */ - free((void*)disc_path); + result = rc_hash_from_file(hash, console_id, &first_file_iterator); + + rc_hash_destroy_iterator(&first_file_iterator); return result; } -int rc_hash_generate_from_file(char hash[33], uint32_t console_id, const char* path) +static int rc_hash_from_file(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator) { - switch (console_id) - { + const char* path = iterator->path; + + switch (console_id) { default: - { - char buffer[128]; - snprintf(buffer, sizeof(buffer), "Unsupported console for file hash: %d", console_id); - return rc_hash_error(buffer); - } + return rc_hash_iterator_error_formatted(iterator, "Unsupported console for file hash: %d", console_id); case RC_CONSOLE_ARCADIA_2001: case RC_CONSOLE_ATARI_2600: @@ -3430,111 +819,141 @@ int rc_hash_generate_from_file(char hash[33], uint32_t console_id, const char* p case RC_CONSOLE_WONDERSWAN: case RC_CONSOLE_ZX_SPECTRUM: /* generic whole-file hash - don't buffer */ - return rc_hash_whole_file(hash, path); + return rc_hash_whole_file(hash, iterator); - case RC_CONSOLE_AMSTRAD_PC: - case RC_CONSOLE_APPLE_II: - case RC_CONSOLE_COMMODORE_64: case RC_CONSOLE_MEGA_DRIVE: - case RC_CONSOLE_MSX: - case RC_CONSOLE_PC8800: /* generic whole-file hash with m3u support - don't buffer */ if (rc_path_compare_extension(path, "m3u")) - return rc_hash_generate_from_playlist(hash, console_id, path); + return rc_hash_generate_from_playlist(hash, console_id, iterator); - return rc_hash_whole_file(hash, path); + return rc_hash_whole_file(hash, iterator); - case RC_CONSOLE_ARDUBOY: case RC_CONSOLE_ATARI_7800: case RC_CONSOLE_ATARI_LYNX: + case RC_CONSOLE_FAMICOM_DISK_SYSTEM: case RC_CONSOLE_NINTENDO: case RC_CONSOLE_PC_ENGINE: case RC_CONSOLE_SUPER_CASSETTEVISION: 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); + return rc_hash_buffered_file(hash, console_id, iterator); + case RC_CONSOLE_AMSTRAD_PC: + case RC_CONSOLE_APPLE_II: + case RC_CONSOLE_COMMODORE_64: + case RC_CONSOLE_MSX: + case RC_CONSOLE_PC8800: + /* generic whole-file hash with m3u support - don't buffer */ + if (rc_path_compare_extension(path, "m3u")) + return rc_hash_generate_from_playlist(hash, console_id, iterator); + + return rc_hash_whole_file(hash, iterator); + +#ifndef RC_HASH_NO_DISC case RC_CONSOLE_3DO: if (rc_path_compare_extension(path, "m3u")) - return rc_hash_generate_from_playlist(hash, console_id, path); + return rc_hash_generate_from_playlist(hash, console_id, iterator); - return rc_hash_3do(hash, path); + return rc_hash_3do(hash, iterator); +#endif +#ifndef RC_HASH_NO_ROM case RC_CONSOLE_ARCADE: - return rc_hash_arcade(hash, path); + return rc_hash_arcade(hash, iterator); + case RC_CONSOLE_ARDUBOY: + return rc_hash_arduboy(hash, iterator); +#endif + +#ifndef RC_HASH_NO_DISC case RC_CONSOLE_ATARI_JAGUAR_CD: - return rc_hash_jaguar_cd(hash, path); + return rc_hash_jaguar_cd(hash, iterator); case RC_CONSOLE_DREAMCAST: if (rc_path_compare_extension(path, "m3u")) - return rc_hash_generate_from_playlist(hash, console_id, path); + return rc_hash_generate_from_playlist(hash, console_id, iterator); - return rc_hash_dreamcast(hash, path); + return rc_hash_dreamcast(hash, iterator); case RC_CONSOLE_GAMECUBE: - return rc_hash_gamecube(hash, path); + return rc_hash_gamecube(hash, iterator); +#endif +#ifndef RC_HASH_NO_ZIP case RC_CONSOLE_MS_DOS: - return rc_hash_ms_dos(hash, path); + return rc_hash_ms_dos(hash, iterator); +#endif +#ifndef RC_HASH_NO_DISC case RC_CONSOLE_NEO_GEO_CD: - return rc_hash_neogeo_cd(hash, path); + return rc_hash_neogeo_cd(hash, iterator); +#endif +#ifndef RC_HASH_NO_ROM case RC_CONSOLE_NINTENDO_64: - return rc_hash_n64(hash, path); + return rc_hash_n64(hash, iterator); +#endif +#ifndef RC_HASH_NO_ENCRYPTED case RC_CONSOLE_NINTENDO_3DS: - return rc_hash_nintendo_3ds(hash, path); + return rc_hash_nintendo_3ds(hash, iterator); +#endif +#ifndef RC_HASH_NO_ROM case RC_CONSOLE_NINTENDO_DS: case RC_CONSOLE_NINTENDO_DSI: - return rc_hash_nintendo_ds(hash, path); + return rc_hash_nintendo_ds(hash, iterator); +#endif +#ifndef RC_HASH_NO_DISC 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); + return rc_hash_pce_cd(hash, iterator); if (rc_path_compare_extension(path, "m3u")) - return rc_hash_generate_from_playlist(hash, console_id, path); + return rc_hash_generate_from_playlist(hash, console_id, iterator); - return rc_hash_buffered_file(hash, console_id, path); + return rc_hash_buffered_file(hash, console_id, iterator); case RC_CONSOLE_PCFX: if (rc_path_compare_extension(path, "m3u")) - return rc_hash_generate_from_playlist(hash, console_id, path); + return rc_hash_generate_from_playlist(hash, console_id, iterator); - return rc_hash_pcfx_cd(hash, path); + return rc_hash_pcfx_cd(hash, iterator); case RC_CONSOLE_PLAYSTATION: if (rc_path_compare_extension(path, "m3u")) - return rc_hash_generate_from_playlist(hash, console_id, path); + return rc_hash_generate_from_playlist(hash, console_id, iterator); - return rc_hash_psx(hash, path); + return rc_hash_psx(hash, iterator); case RC_CONSOLE_PLAYSTATION_2: if (rc_path_compare_extension(path, "m3u")) - return rc_hash_generate_from_playlist(hash, console_id, path); + return rc_hash_generate_from_playlist(hash, console_id, iterator); - return rc_hash_ps2(hash, path); + return rc_hash_ps2(hash, iterator); case RC_CONSOLE_PSP: - return rc_hash_psp(hash, path); + return rc_hash_psp(hash, iterator); case RC_CONSOLE_SEGA_CD: case RC_CONSOLE_SATURN: if (rc_path_compare_extension(path, "m3u")) - return rc_hash_generate_from_playlist(hash, console_id, path); + return rc_hash_generate_from_playlist(hash, console_id, iterator); - return rc_hash_sega_cd(hash, path); + return rc_hash_sega_cd(hash, iterator); + + case RC_CONSOLE_WII: + return rc_hash_wii(hash, iterator); +#endif } } -static void rc_hash_iterator_append_console(struct rc_hash_iterator* iterator, uint8_t console_id) -{ +static void rc_hash_initialize_iterator_from_path(rc_hash_iterator_t* iterator, const char* path); + +static void rc_hash_iterator_append_console(struct rc_hash_iterator* iterator, uint8_t console_id) { int i = 0; - while (iterator->consoles[i] != 0) - { + while (iterator->consoles[i] != 0) { if (iterator->consoles[i] == console_id) return; @@ -3544,47 +963,149 @@ static void rc_hash_iterator_append_console(struct rc_hash_iterator* iterator, u iterator->consoles[i] = console_id; } -static void rc_hash_initialize_dsk_iterator(struct rc_hash_iterator* iterator, const char* path) +void rc_hash_merge_callbacks(rc_hash_iterator_t* iterator, const rc_hash_callbacks_t* callbacks) { - size_t size = iterator->buffer_size; - if (size == 0) - { - /* attempt to use disk size to determine system */ - void* file = rc_file_open(path); - if (file) - { - rc_file_seek(file, 0, SEEK_END); - size = (size_t)rc_file_tell(file); - rc_file_close(file); + if (callbacks->verbose_message) + iterator->callbacks.verbose_message = callbacks->verbose_message; + if (callbacks->error_message) + iterator->callbacks.verbose_message = callbacks->error_message; + + if (callbacks->filereader.open) + memcpy(&iterator->callbacks.filereader, &callbacks->filereader, sizeof(callbacks->filereader)); + +#ifndef RC_HASH_NO_DISC + if (callbacks->cdreader.open_track) + memcpy(&iterator->callbacks.cdreader, &callbacks->cdreader, sizeof(callbacks->cdreader)); +#endif + +#ifndef RC_HASH_NO_ENCRYPTED + if (callbacks->encryption.get_3ds_cia_normal_key) + iterator->callbacks.encryption.get_3ds_cia_normal_key = callbacks->encryption.get_3ds_cia_normal_key; + if (callbacks->encryption.get_3ds_ncch_normal_keys) + iterator->callbacks.encryption.get_3ds_ncch_normal_keys = callbacks->encryption.get_3ds_ncch_normal_keys; +#endif +} + + +static void rc_hash_reset_iterator(rc_hash_iterator_t* iterator) { + memset(iterator, 0, sizeof(*iterator)); + iterator->index = -1; + + if (g_verbose_message_callback) + iterator->callbacks.verbose_message = rc_hash_call_g_verbose_message_callback; + if (g_error_message_callback) + iterator->callbacks.error_message = rc_hash_call_g_error_message_callback; + + if (g_filereader) { + memcpy(&iterator->callbacks.filereader, g_filereader, sizeof(*g_filereader)); + } else if (!iterator->callbacks.filereader.open) { + iterator->callbacks.filereader.open = filereader_open; + iterator->callbacks.filereader.close = filereader_close; + iterator->callbacks.filereader.seek = filereader_seek; + iterator->callbacks.filereader.tell = filereader_tell; + iterator->callbacks.filereader.read = filereader_read; + } + +#ifndef RC_HASH_NO_DISC + rc_hash_reset_iterator_disc(iterator); +#endif + +#ifndef RC_HASH_NO_ENCRYPTED + rc_hash_reset_iterator_encrypted(iterator); +#endif +} + +static void rc_hash_initialize_iterator_single(rc_hash_iterator_t* iterator, int data) { + iterator->consoles[0] = (uint8_t)data; +} + +static void rc_hash_initialize_iterator_bin(rc_hash_iterator_t* iterator, int data) { + (void)data; + + if (iterator->buffer_size == 0) { + /* raw bin file may be a CD track. if it's more than 32MB, try a CD hash. */ + const int64_t size = rc_file_size(iterator, iterator->path); + if (size > 32 * 1024 * 1024) { + iterator->consoles[0] = RC_CONSOLE_3DO; /* 4DO supports directly opening the bin file */ + iterator->consoles[1] = RC_CONSOLE_PLAYSTATION; /* PCSX ReARMed supports directly opening the bin file*/ + iterator->consoles[2] = RC_CONSOLE_PLAYSTATION_2; /* PCSX2 supports directly opening the bin file*/ + iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* Genesis Plus GX supports directly opening the bin file*/ + + /* fallback to megadrive which just does a full hash. */ + iterator->consoles[4] = RC_CONSOLE_MEGA_DRIVE; + return; } } - if (size == 512 * 9 * 80) /* 360KB */ - { + /* bin is associated with MegaDrive, Sega32X, Atari 2600, Watara Supervision, MegaDuck, + * Fairchild Channel F, Arcadia 2001, Interton VC 4000, and Super Cassette Vision. + * Since they all use the same hashing algorithm, only specify one of them */ + iterator->consoles[0] = RC_CONSOLE_MEGA_DRIVE; +} + +static void rc_hash_initialize_iterator_chd(rc_hash_iterator_t* iterator, int data) { + (void)data; + + iterator->consoles[0] = RC_CONSOLE_PLAYSTATION; + 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_PSP; + iterator->consoles[5] = RC_CONSOLE_PC_ENGINE_CD; + iterator->consoles[6] = RC_CONSOLE_3DO; + iterator->consoles[7] = RC_CONSOLE_NEO_GEO_CD; + iterator->consoles[8] = RC_CONSOLE_PCFX; +} + +static void rc_hash_initialize_iterator_cue(rc_hash_iterator_t* iterator, int data) { + (void)data; + + iterator->consoles[0] = RC_CONSOLE_PLAYSTATION; + 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_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; +} + +static void rc_hash_initialize_iterator_d88(rc_hash_iterator_t* iterator, int data) { + (void)data; + + iterator->consoles[0] = RC_CONSOLE_PC8800; + iterator->consoles[1] = RC_CONSOLE_SHARPX1; +} + +static void rc_hash_initialize_iterator_dsk(rc_hash_iterator_t* iterator, int data) { + size_t size = iterator->buffer_size; + if (size == 0) + size = (size_t)rc_file_size(iterator, iterator->path); + + (void)data; + + if (size == 512 * 9 * 80) { /* 360KB */ /* FAT-12 3.5" DD (512 byte sectors, 9 sectors per track, 80 tracks per side */ /* FAT-12 5.25" DD double-sided (512 byte sectors, 9 sectors per track, 80 tracks per side */ iterator->consoles[0] = RC_CONSOLE_MSX; } - else if (size == 512 * 9 * 80 * 2) /* 720KB */ - { + else if (size == 512 * 9 * 80 * 2) { /* 720KB */ /* FAT-12 3.5" DD double-sided (512 byte sectors, 9 sectors per track, 80 tracks per side */ iterator->consoles[0] = RC_CONSOLE_MSX; } - else if (size == 512 * 9 * 40) /* 180KB */ - { + else if (size == 512 * 9 * 40) { /* 180KB */ /* FAT-12 5.25" DD (512 byte sectors, 9 sectors per track, 40 tracks per side */ iterator->consoles[0] = RC_CONSOLE_MSX; /* AMSDOS 3" - 40 tracks */ iterator->consoles[1] = RC_CONSOLE_AMSTRAD_PC; } - else if (size == 256 * 16 * 35) /* 140KB */ - { + else if (size == 256 * 16 * 35) { /* 140KB */ /* Apple II new format - 256 byte sectors, 16 sectors per track, 35 tracks per side */ iterator->consoles[0] = RC_CONSOLE_APPLE_II; } - else if (size == 256 * 13 * 35) /* 113.75KB */ - { + else if (size == 256 * 13 * 35) { /* 113.75KB */ /* Apple II old format - 256 byte sectors, 13 sectors per track, 35 tracks per side */ iterator->consoles[0] = RC_CONSOLE_APPLE_II; } @@ -3598,521 +1119,278 @@ 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, const uint8_t* buffer, size_t buffer_size) -{ - int need_path = !buffer; +static void rc_hash_initialize_iterator_iso(rc_hash_iterator_t* iterator, int data) { + (void)data; - memset(iterator, 0, sizeof(*iterator)); + iterator->consoles[0] = RC_CONSOLE_PLAYSTATION_2; + iterator->consoles[1] = RC_CONSOLE_PSP; + iterator->consoles[2] = RC_CONSOLE_3DO; + iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ + iterator->consoles[4] = RC_CONSOLE_GAMECUBE; + iterator->consoles[5] = RC_CONSOLE_WII; +} + +static void rc_hash_initialize_iterator_m3u(rc_hash_iterator_t* iterator, int data) { + const char* first_file_path; + + (void)data; + + /* temporarily set the iterator path to the m3u file so we can extract the + * path of the first disc. rc_hash_get_first_item_from_playlist will return + * an allocated string or NULL, so rc_hash_destroy_iterator won't get tripped + * up by the non-allocted value we're about to assign. + */ + first_file_path = rc_hash_get_first_item_from_playlist(iterator); + if (!first_file_path) /* did not find a disc */ + return; + + /* release the m3u path and replace with the first file path */ + free((void*)iterator->path); + iterator->path = first_file_path; /* assert: already malloc'd; don't need to strdup */ + + iterator->buffer = NULL; /* ignore buffer; assume it's the m3u contents */ + + rc_hash_initialize_iterator_from_path(iterator, iterator->path); +} + +static void rc_hash_initialize_iterator_nib(rc_hash_iterator_t* iterator, int data) { + (void)data; + + iterator->consoles[0] = RC_CONSOLE_APPLE_II; + iterator->consoles[1] = RC_CONSOLE_COMMODORE_64; +} + +static void rc_hash_initialize_iterator_rom(rc_hash_iterator_t* iterator, int data) { + (void)data; + + /* rom is associated with MSX, Thomson TO-8, and Fairchild Channel F. + * Since they all use the same hashing algorithm, only specify one of them */ + iterator->consoles[0] = RC_CONSOLE_MSX; +} + +static void rc_hash_initialize_iterator_tap(rc_hash_iterator_t* iterator, int data) { + (void)data; + + /* also Oric and ZX Spectrum, but all are full file hashes */ + iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; +} + +static const rc_hash_iterator_ext_handler_entry_t rc_hash_iterator_ext_handlers[] = { + { "2d", rc_hash_initialize_iterator_single, RC_CONSOLE_SHARPX1 }, + { "3ds", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "3dsx", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "7z", rc_hash_initialize_iterator_single, RC_CONSOLE_ARCADE }, + { "83g", rc_hash_initialize_iterator_single, RC_CONSOLE_TI83 }, /* http://tibasicdev.wikidot.com/file-extensions */ + { "83p", rc_hash_initialize_iterator_single, RC_CONSOLE_TI83 }, + { "a26", rc_hash_initialize_iterator_single, RC_CONSOLE_ATARI_2600 }, + { "a78", rc_hash_initialize_iterator_single, RC_CONSOLE_ATARI_7800 }, + { "app", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "arduboy", rc_hash_initialize_iterator_single, RC_CONSOLE_ARDUBOY }, + { "axf", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "bin", rc_hash_initialize_iterator_bin, 0 }, + { "bs", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_NINTENDO }, + { "cart", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_CASSETTEVISION }, + { "cas", rc_hash_initialize_iterator_single, RC_CONSOLE_MSX }, + { "cci", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "chd", rc_hash_initialize_iterator_chd, 0 }, + { "chf", rc_hash_initialize_iterator_single, RC_CONSOLE_FAIRCHILD_CHANNEL_F }, + { "cia", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "col", rc_hash_initialize_iterator_single, RC_CONSOLE_COLECOVISION }, + { "csw", rc_hash_initialize_iterator_single, RC_CONSOLE_ZX_SPECTRUM }, + { "cue", rc_hash_initialize_iterator_cue, 0 }, + { "cxi", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "d64", rc_hash_initialize_iterator_single, RC_CONSOLE_COMMODORE_64 }, + { "d88", rc_hash_initialize_iterator_d88, 0 }, + { "dosz", rc_hash_initialize_iterator_single, RC_CONSOLE_MS_DOS }, + { "dsk", rc_hash_initialize_iterator_dsk, 0 }, + { "elf", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_3DS }, + { "fd", rc_hash_initialize_iterator_single, RC_CONSOLE_THOMSONTO8 }, + { "fds", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO }, + { "fig", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_NINTENDO }, + { "gb", rc_hash_initialize_iterator_single, RC_CONSOLE_GAMEBOY }, + { "gba", rc_hash_initialize_iterator_single, RC_CONSOLE_GAMEBOY_ADVANCE }, + { "gbc", rc_hash_initialize_iterator_single, RC_CONSOLE_GAMEBOY_COLOR }, + { "gdi", rc_hash_initialize_iterator_single, RC_CONSOLE_DREAMCAST }, + { "gg", rc_hash_initialize_iterator_single, RC_CONSOLE_GAME_GEAR }, + { "hex", rc_hash_initialize_iterator_single, RC_CONSOLE_ARDUBOY }, + { "iso", rc_hash_initialize_iterator_iso, 0 }, + { "jag", rc_hash_initialize_iterator_single, RC_CONSOLE_ATARI_JAGUAR }, + { "k7", rc_hash_initialize_iterator_single, RC_CONSOLE_THOMSONTO8 }, /* tape */ + { "lnx", rc_hash_initialize_iterator_single, RC_CONSOLE_ATARI_LYNX }, + { "m3u", rc_hash_initialize_iterator_m3u, 0 }, + { "m5", rc_hash_initialize_iterator_single, RC_CONSOLE_THOMSONTO8 }, /* cartridge */ + { "m7", rc_hash_initialize_iterator_single, RC_CONSOLE_THOMSONTO8 }, /* cartridge */ + { "md", rc_hash_initialize_iterator_single, RC_CONSOLE_MEGA_DRIVE }, + { "min", rc_hash_initialize_iterator_single, RC_CONSOLE_POKEMON_MINI }, + { "mx1", rc_hash_initialize_iterator_single, RC_CONSOLE_MSX }, + { "mx2", rc_hash_initialize_iterator_single, RC_CONSOLE_MSX }, + { "n64", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_64 }, + { "ndd", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_64 }, + { "nds", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_DS }, /* handles both DS and DSi */ + { "nes", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO }, + { "ngc", rc_hash_initialize_iterator_single, RC_CONSOLE_NEOGEO_POCKET }, + { "nib", rc_hash_initialize_iterator_nib, 0 }, + { "pbp", rc_hash_initialize_iterator_single, RC_CONSOLE_PSP }, + { "pce", rc_hash_initialize_iterator_single, RC_CONSOLE_PC_ENGINE }, + { "pgm", rc_hash_initialize_iterator_single, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER }, + { "pzx", rc_hash_initialize_iterator_single, RC_CONSOLE_ZX_SPECTRUM }, + { "ri", rc_hash_initialize_iterator_single, RC_CONSOLE_MSX }, + { "rom", rc_hash_initialize_iterator_rom, 0 }, + { "sap", rc_hash_initialize_iterator_single, RC_CONSOLE_THOMSONTO8 }, /* disk */ + { "scl", rc_hash_initialize_iterator_single, RC_CONSOLE_ZX_SPECTRUM }, + { "sfc", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_NINTENDO }, + { "sg", rc_hash_initialize_iterator_single, RC_CONSOLE_SG1000 }, + { "sgx", rc_hash_initialize_iterator_single, RC_CONSOLE_PC_ENGINE }, + { "smc", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_NINTENDO }, + { "sv", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPERVISION }, + { "swc", rc_hash_initialize_iterator_single, RC_CONSOLE_SUPER_NINTENDO }, + { "tap", rc_hash_initialize_iterator_tap, 0 }, + { "tic", rc_hash_initialize_iterator_single, RC_CONSOLE_TIC80 }, + { "trd", rc_hash_initialize_iterator_single, RC_CONSOLE_ZX_SPECTRUM }, + { "tvc", rc_hash_initialize_iterator_single, RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER }, + { "tzx", rc_hash_initialize_iterator_single, RC_CONSOLE_ZX_SPECTRUM }, + { "uze", rc_hash_initialize_iterator_single, RC_CONSOLE_UZEBOX }, + { "v64", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_64 }, + { "vb", rc_hash_initialize_iterator_single, RC_CONSOLE_VIRTUAL_BOY }, + { "wad", rc_hash_initialize_iterator_single, RC_CONSOLE_WII }, + { "wasm", rc_hash_initialize_iterator_single, RC_CONSOLE_WASM4 }, + { "woz", rc_hash_initialize_iterator_single, RC_CONSOLE_APPLE_II }, + { "wsc", rc_hash_initialize_iterator_single, RC_CONSOLE_WONDERSWAN }, + { "z64", rc_hash_initialize_iterator_single, RC_CONSOLE_NINTENDO_64 }, + { "zip", rc_hash_initialize_iterator_single, RC_CONSOLE_ARCADE } +}; + +const rc_hash_iterator_ext_handler_entry_t* rc_hash_get_iterator_ext_handlers(size_t* num_handlers) { + *num_handlers = sizeof(rc_hash_iterator_ext_handlers) / sizeof(rc_hash_iterator_ext_handlers[0]); + return rc_hash_iterator_ext_handlers; +} + +static int rc_hash_iterator_find_handler(const void* left, const void* right) { + const rc_hash_iterator_ext_handler_entry_t* left_handler = + (const rc_hash_iterator_ext_handler_entry_t*)left; + const rc_hash_iterator_ext_handler_entry_t* right_handler = + (const rc_hash_iterator_ext_handler_entry_t*)right; + + return strcmp(left_handler->ext, right_handler->ext); +} + +static void rc_hash_initialize_iterator_from_path(rc_hash_iterator_t* iterator, const char* path) { + size_t num_handlers; + const rc_hash_iterator_ext_handler_entry_t* handlers = rc_hash_get_iterator_ext_handlers(&num_handlers); + const rc_hash_iterator_ext_handler_entry_t* handler; + rc_hash_iterator_ext_handler_entry_t search; + const char* ext = rc_path_get_extension(path); + size_t index; + + /* lowercase the extension as we copy it into the search object */ + memset(&search, 0, sizeof(search)); + for (index = 0; index < sizeof(search.ext) - 1; ++index) { + const int c = (int)ext[index]; + if (!c) + break; + + search.ext[index] = tolower(c); + } + + /* find the handler for the extension */ + handler = bsearch(&search, handlers, num_handlers, sizeof(*handler), rc_hash_iterator_find_handler); + if (handler) { + handler->handler(iterator, handler->data); + } else { + /* if we didn't match the extension, default to something that does a whole file hash */ + if (!iterator->consoles[0]) + iterator->consoles[0] = RC_CONSOLE_GAMEBOY; + } +} + +void rc_hash_initialize_iterator(rc_hash_iterator_t* iterator, const char* path, const uint8_t* buffer, size_t buffer_size) +{ + rc_hash_reset_iterator(iterator); iterator->buffer = buffer; iterator->buffer_size = buffer_size; - iterator->consoles[0] = 0; + if (path) + iterator->path = strdup(path); +} - do - { - const char* ext = rc_path_get_extension(path); - switch (tolower(*ext)) - { - case '2': - if (rc_path_compare_extension(ext, "2d")) - { - iterator->consoles[0] = RC_CONSOLE_SHARPX1; - } - break; +void rc_hash_destroy_iterator(rc_hash_iterator_t* iterator) { + if (iterator->path) { + free((void*)iterator->path); + iterator->path = NULL; + } - case '3': - if (rc_path_compare_extension(ext, "3ds") || - rc_path_compare_extension(ext, "3dsx")) - { - iterator->consoles[0] = RC_CONSOLE_NINTENDO_3DS; - } - break; + iterator->buffer = NULL; +} - case '7': - if (rc_path_compare_extension(ext, "7z")) - { - /* decompressing zip file not supported */ - iterator->consoles[0] = RC_CONSOLE_ARCADE; - need_path = 1; - } - break; +int rc_hash_iterate(char hash[33], rc_hash_iterator_t* iterator) { + int next_console; + int result = 0; - 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; + if (iterator->index == -1) { + rc_hash_initialize_iterator_from_path(iterator, iterator->path); - case 'a': - if (rc_path_compare_extension(ext, "a78")) - { - iterator->consoles[0] = RC_CONSOLE_ATARI_7800; - } - else if (rc_path_compare_extension(ext, "app") || - rc_path_compare_extension(ext, "axf")) - { - iterator->consoles[0] = RC_CONSOLE_NINTENDO_3DS; - } - break; - - case 'b': - if (rc_path_compare_extension(ext, "bin")) - { - if (buffer_size == 0) - { - /* raw bin file may be a CD track. if it's more than 32MB, try a CD hash. */ - void* file = rc_file_open(path); - if (file) - { - int64_t size; - - rc_file_seek(file, 0, SEEK_END); - size = rc_file_tell(file); - rc_file_close(file); - - if (size > 32 * 1024 * 1024) - { - iterator->consoles[0] = RC_CONSOLE_3DO; /* 4DO supports directly opening the bin file */ - iterator->consoles[1] = RC_CONSOLE_PLAYSTATION; /* PCSX ReARMed supports directly opening the bin file*/ - iterator->consoles[2] = RC_CONSOLE_PLAYSTATION_2; /* PCSX2 supports directly opening the bin file*/ - iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* Genesis Plus GX supports directly opening the bin file*/ - - /* fallback to megadrive which just does a full hash. */ - iterator->consoles[4] = RC_CONSOLE_MEGA_DRIVE; - break; - } - } - } - - /* bin is associated with MegaDrive, Sega32X, Atari 2600, Watara Supervision, MegaDuck, - * Fairchild Channel F, Arcadia 2001, Interton VC 4000, and Super Cassette Vision. - * Since they all use the same hashing algorithm, only specify one of them */ - iterator->consoles[0] = RC_CONSOLE_MEGA_DRIVE; - } - else if (rc_path_compare_extension(ext, "bs")) - { - iterator->consoles[0] = RC_CONSOLE_SUPER_NINTENDO; - } - break; - - case 'c': - if (rc_path_compare_extension(ext, "cue")) - { - iterator->consoles[0] = RC_CONSOLE_PLAYSTATION; - 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_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")) - { - iterator->consoles[0] = RC_CONSOLE_PLAYSTATION; - 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_PSP; - iterator->consoles[5] = RC_CONSOLE_PC_ENGINE_CD; - iterator->consoles[6] = RC_CONSOLE_3DO; - iterator->consoles[7] = RC_CONSOLE_NEO_GEO_CD; - iterator->consoles[8] = RC_CONSOLE_PCFX; - need_path = 1; - } - else if (rc_path_compare_extension(ext, "col")) - { - iterator->consoles[0] = RC_CONSOLE_COLECOVISION; - } - else if (rc_path_compare_extension(ext, "cas")) - { - iterator->consoles[0] = RC_CONSOLE_MSX; - } - else if (rc_path_compare_extension(ext, "chf")) - { - iterator->consoles[0] = RC_CONSOLE_FAIRCHILD_CHANNEL_F; - } - else if (rc_path_compare_extension(ext, "cart")) - { - iterator->consoles[0] = RC_CONSOLE_SUPER_CASSETTEVISION; - } - else if (rc_path_compare_extension(ext, "cci") || - rc_path_compare_extension(ext, "cia") || - rc_path_compare_extension(ext, "cxi")) - { - iterator->consoles[0] = RC_CONSOLE_NINTENDO_3DS; - } - else if (rc_path_compare_extension(ext, "csw")) - { - iterator->consoles[0] = RC_CONSOLE_ZX_SPECTRUM; - } - break; - - case 'd': - if (rc_path_compare_extension(ext, "dsk")) - { - rc_hash_initialize_dsk_iterator(iterator, path); - } - else if (rc_path_compare_extension(ext, "d64")) - { - iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; - } - else if (rc_path_compare_extension(ext, "d88")) - { - iterator->consoles[0] = RC_CONSOLE_PC8800; - iterator->consoles[1] = RC_CONSOLE_SHARPX1; - } - else if (rc_path_compare_extension(ext, "dosz")) - { - iterator->consoles[0] = RC_CONSOLE_MS_DOS; - } - break; - - case 'e': - if (rc_path_compare_extension(ext, "elf")) - { - /* This should probably apply to more consoles in the future */ - /* Although in any case this just hashes the entire file */ - iterator->consoles[0] = RC_CONSOLE_NINTENDO_3DS; - } - break; - - case 'f': - if (rc_path_compare_extension(ext, "fig")) - { - iterator->consoles[0] = RC_CONSOLE_SUPER_NINTENDO; - } - else if (rc_path_compare_extension(ext, "fds")) - { - iterator->consoles[0] = RC_CONSOLE_NINTENDO; - } - else if (rc_path_compare_extension(ext, "fd")) - { - iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ - } - break; - - case 'g': - if (rc_path_compare_extension(ext, "gba")) - { - iterator->consoles[0] = RC_CONSOLE_GAMEBOY_ADVANCE; - } - else if (rc_path_compare_extension(ext, "gbc")) - { - iterator->consoles[0] = RC_CONSOLE_GAMEBOY_COLOR; - } - else if (rc_path_compare_extension(ext, "gb")) - { - iterator->consoles[0] = RC_CONSOLE_GAMEBOY; - } - else if (rc_path_compare_extension(ext, "gg")) - { - iterator->consoles[0] = RC_CONSOLE_GAME_GEAR; - } - else if (rc_path_compare_extension(ext, "gdi")) - { - iterator->consoles[0] = RC_CONSOLE_DREAMCAST; - } - break; - - case 'h': - if (rc_path_compare_extension(ext, "hex")) - { - iterator->consoles[0] = RC_CONSOLE_ARDUBOY; - } - break; - - case 'i': - if (rc_path_compare_extension(ext, "iso")) - { - iterator->consoles[0] = RC_CONSOLE_PLAYSTATION_2; - iterator->consoles[1] = RC_CONSOLE_PSP; - iterator->consoles[2] = RC_CONSOLE_3DO; - iterator->consoles[3] = RC_CONSOLE_SEGA_CD; /* ASSERT: handles both Sega CD and Saturn */ - iterator->consoles[4] = RC_CONSOLE_GAMECUBE; - need_path = 1; - } - break; - - case 'j': - if (rc_path_compare_extension(ext, "jag")) - { - iterator->consoles[0] = RC_CONSOLE_ATARI_JAGUAR; - } - break; - - case 'k': - if (rc_path_compare_extension(ext, "k7")) - { - iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* tape */ - } - break; - - case 'l': - if (rc_path_compare_extension(ext, "lnx")) - { - iterator->consoles[0] = RC_CONSOLE_ATARI_LYNX; - } - break; - - case 'm': - if (rc_path_compare_extension(ext, "m3u")) - { - const char* disc_path = rc_hash_get_first_item_from_playlist(path); - if (!disc_path) /* did not find a disc */ - return; - - iterator->buffer = NULL; /* ignore buffer; assume it's the m3u contents */ - - path = iterator->path = disc_path; - continue; /* retry with disc_path */ - } - else if (rc_path_compare_extension(ext, "md")) - { - iterator->consoles[0] = RC_CONSOLE_MEGA_DRIVE; - } - else if (rc_path_compare_extension(ext, "min")) - { - iterator->consoles[0] = RC_CONSOLE_POKEMON_MINI; - } - else if (rc_path_compare_extension(ext, "mx1")) - { - iterator->consoles[0] = RC_CONSOLE_MSX; - } - else if (rc_path_compare_extension(ext, "mx2")) - { - iterator->consoles[0] = RC_CONSOLE_MSX; - } - else if (rc_path_compare_extension(ext, "m5")) - { - iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* cartridge */ - } - else if (rc_path_compare_extension(ext, "m7")) - { - iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* cartridge */ - } - break; - - case 'n': - if (rc_path_compare_extension(ext, "nes")) - { - iterator->consoles[0] = RC_CONSOLE_NINTENDO; - } - else if (rc_path_compare_extension(ext, "nds")) - { - 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")) - { - iterator->consoles[0] = RC_CONSOLE_NINTENDO_64; - } - else if (rc_path_compare_extension(ext, "ngc")) - { - iterator->consoles[0] = RC_CONSOLE_NEOGEO_POCKET; - } - else if (rc_path_compare_extension(ext, "nib")) - { - /* also Apple II, but both are full-file hashes */ - iterator->consoles[0] = RC_CONSOLE_COMMODORE_64; - } - break; - - case 'p': - if (rc_path_compare_extension(ext, "pce")) - { - iterator->consoles[0] = RC_CONSOLE_PC_ENGINE; - } - else if (rc_path_compare_extension(ext, "pbp")) - { - iterator->consoles[0] = RC_CONSOLE_PSP; - } - else if (rc_path_compare_extension(ext, "pgm")) - { - iterator->consoles[0] = RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER; - } - else if (rc_path_compare_extension(ext, "pzx")) - { - iterator->consoles[0] = RC_CONSOLE_ZX_SPECTRUM; - } - break; - - case 'r': - if (rc_path_compare_extension(ext, "rom")) - { - /* rom is associated with MSX, Thomson TO-8, and Fairchild Channel F. - * Since they all use the same hashing algorithm, only specify one of them */ - iterator->consoles[0] = RC_CONSOLE_MSX; - } - if (rc_path_compare_extension(ext, "ri")) - { - iterator->consoles[0] = RC_CONSOLE_MSX; - } - break; - - case 's': - if (rc_path_compare_extension(ext, "smc") || - rc_path_compare_extension(ext, "sfc") || - rc_path_compare_extension(ext, "swc")) - { - iterator->consoles[0] = RC_CONSOLE_SUPER_NINTENDO; - } - else if (rc_path_compare_extension(ext, "sg")) - { - iterator->consoles[0] = RC_CONSOLE_SG1000; - } - else if (rc_path_compare_extension(ext, "sgx")) - { - iterator->consoles[0] = RC_CONSOLE_PC_ENGINE; - } - else if (rc_path_compare_extension(ext, "sv")) - { - iterator->consoles[0] = RC_CONSOLE_SUPERVISION; - } - else if (rc_path_compare_extension(ext, "sap")) - { - iterator->consoles[0] = RC_CONSOLE_THOMSONTO8; /* disk */ - } - else if (rc_path_compare_extension(ext, "scl")) - { - iterator->consoles[0] = RC_CONSOLE_ZX_SPECTRUM; - } - break; - - case 't': - if (rc_path_compare_extension(ext, "tap")) - { - /* also Commodore 64 and ZX Spectrum, but all are full file hashes */ - iterator->consoles[0] = RC_CONSOLE_ORIC; - } - else if (rc_path_compare_extension(ext, "tic")) - { - iterator->consoles[0] = RC_CONSOLE_TIC80; - } - else if (rc_path_compare_extension(ext, "tvc")) - { - iterator->consoles[0] = RC_CONSOLE_ELEKTOR_TV_GAMES_COMPUTER; - } - else if (rc_path_compare_extension(ext, "trd") || - rc_path_compare_extension(ext, "tzx")) - { - iterator->consoles[0] = RC_CONSOLE_ZX_SPECTRUM; - } - 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")) - { - iterator->consoles[0] = RC_CONSOLE_VIRTUAL_BOY; - } - else if (rc_path_compare_extension(ext, "v64")) - { - iterator->consoles[0] = RC_CONSOLE_NINTENDO_64; - } - break; - - case 'w': - if (rc_path_compare_extension(ext, "wsc")) - { - iterator->consoles[0] = RC_CONSOLE_WONDERSWAN; - } - else if (rc_path_compare_extension(ext, "wasm")) - { - iterator->consoles[0] = RC_CONSOLE_WASM4; - } - else if (rc_path_compare_extension(ext, "woz")) - { - iterator->consoles[0] = RC_CONSOLE_APPLE_II; - } - break; - - case 'z': - if (rc_path_compare_extension(ext, "zip")) - { - /* decompressing zip file not supported */ - iterator->consoles[0] = RC_CONSOLE_ARCADE; - need_path = 1; - } - else if (rc_path_compare_extension(ext, "z64")) - { - iterator->consoles[0] = RC_CONSOLE_NINTENDO_64; - } - break; - } - - if (verbose_message_callback) - { - char message[256]; + if (iterator->callbacks.verbose_message) { int count = 0; while (iterator->consoles[count]) ++count; - snprintf(message, sizeof(message), "Found %d potential consoles for %s file extension", count, ext); - verbose_message_callback(message); + rc_hash_iterator_verbose_formatted(iterator, "Found %d potential consoles for %s file extension", count, rc_path_get_extension(iterator->path)); } - /* loop is only for specific cases that redirect to another file - like m3u */ - break; - } while (1); - - if (need_path && !iterator->path) - iterator->path = strdup(path); - - /* if we didn't match the extension, default to something that does a whole file hash */ - if (!iterator->consoles[0]) - iterator->consoles[0] = RC_CONSOLE_GAMEBOY; -} - -void rc_hash_destroy_iterator(struct rc_hash_iterator* iterator) -{ - if (iterator->path) - { - free((void*)iterator->path); - iterator->path = NULL; + iterator->index = 0; } -} -int rc_hash_iterate(char hash[33], struct rc_hash_iterator* iterator) -{ - int next_console; - int result = 0; - - do - { + do { next_console = iterator->consoles[iterator->index]; - if (next_console == 0) - { + if (next_console == 0) { hash[0] = '\0'; break; } ++iterator->index; - if (verbose_message_callback) - { - char message[128]; - snprintf(message, sizeof(message), "Trying console %d", next_console); - verbose_message_callback(message); - } - - if (iterator->buffer) - result = rc_hash_generate_from_buffer(hash, next_console, iterator->buffer, iterator->buffer_size); - else - result = rc_hash_generate_from_file(hash, next_console, iterator->path); + rc_hash_iterator_verbose_formatted(iterator, "Trying console %d", next_console); + result = rc_hash_generate(hash, next_console, iterator); } while (!result); return result; } + +int rc_hash_generate(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator) { + if (iterator->buffer) + return rc_hash_from_buffer(hash, console_id, iterator); + + return rc_hash_from_file(hash, console_id, iterator); +} + +int rc_hash_generate_from_buffer(char hash[33], uint32_t console_id, const uint8_t* buffer, size_t buffer_size) { + rc_hash_iterator_t iterator; + int result; + + rc_hash_reset_iterator(&iterator); + iterator.buffer = buffer; + iterator.buffer_size = buffer_size; + + result = rc_hash_from_buffer(hash, console_id, &iterator); + + rc_hash_destroy_iterator(&iterator); + + return result; +} + +int rc_hash_generate_from_file(char hash[33], uint32_t console_id, const char* path){ + rc_hash_iterator_t iterator; + int result; + + rc_hash_reset_iterator(&iterator); + iterator.path = path; + + result = rc_hash_from_file(hash, console_id, &iterator); + + iterator.path = NULL; /* prevent free. we didn't strdup */ + + rc_hash_destroy_iterator(&iterator); + + return result; +} diff --git a/deps/rcheevos/src/rhash/hash_disc.c b/deps/rcheevos/src/rhash/hash_disc.c new file mode 100644 index 0000000000..83c93545e3 --- /dev/null +++ b/deps/rcheevos/src/rhash/hash_disc.c @@ -0,0 +1,1340 @@ +#include "rc_hash.h" + +#include "rc_hash_internal.h" + +#include "../rc_compat.h" + +#include + +/* ===================================================== */ + +static struct rc_hash_cdreader g_cdreader_funcs; +static struct rc_hash_cdreader* g_cdreader = NULL; + +void rc_hash_reset_iterator_disc(rc_hash_iterator_t* iterator) +{ + if (g_cdreader) + memcpy(&iterator->callbacks.cdreader, g_cdreader, sizeof(*g_cdreader)); + else + rc_hash_get_default_cdreader(&iterator->callbacks.cdreader); +} + +void rc_hash_init_custom_cdreader(struct rc_hash_cdreader* reader) +{ + if (reader) { + memcpy(&g_cdreader_funcs, reader, sizeof(g_cdreader_funcs)); + g_cdreader = &g_cdreader_funcs; + } + else { + g_cdreader = NULL; + } +} + +static void* rc_cd_open_track(const rc_hash_iterator_t* iterator, uint32_t track) +{ + if (iterator->callbacks.cdreader.open_track_iterator) + return iterator->callbacks.cdreader.open_track_iterator(iterator->path, track, iterator); + + if (iterator->callbacks.cdreader.open_track) + return iterator->callbacks.cdreader.open_track(iterator->path, track); + + if (g_cdreader && g_cdreader->open_track) + return g_cdreader->open_track(iterator->path, track); + + rc_hash_iterator_error(iterator, "no hook registered for cdreader_open_track"); + return NULL; +} + +static size_t rc_cd_read_sector(const rc_hash_iterator_t* iterator, void* track_handle, uint32_t sector, void* buffer, size_t requested_bytes) +{ + if (iterator->callbacks.cdreader.read_sector) + return iterator->callbacks.cdreader.read_sector(track_handle, sector, buffer, requested_bytes); + + if (g_cdreader && g_cdreader->read_sector) + return g_cdreader->read_sector(track_handle, sector, buffer, requested_bytes); + + rc_hash_iterator_error(iterator, "no hook registered for cdreader_read_sector"); + return 0; +} + +static uint32_t rc_cd_first_track_sector(const rc_hash_iterator_t* iterator, void* track_handle) +{ + if (iterator->callbacks.cdreader.first_track_sector) + return iterator->callbacks.cdreader.first_track_sector(track_handle); + + if (g_cdreader && g_cdreader->first_track_sector) + return g_cdreader->first_track_sector(track_handle); + + rc_hash_iterator_error(iterator, "no hook registered for cdreader_first_track_sector"); + return 0; +} + +static void rc_cd_close_track(const rc_hash_iterator_t* iterator, void* track_handle) +{ + if (iterator->callbacks.cdreader.close_track) + iterator->callbacks.cdreader.close_track(track_handle); + else if (g_cdreader && g_cdreader->close_track) + g_cdreader->close_track(track_handle); + else + rc_hash_iterator_error(iterator, "no hook registered for cdreader_close_track"); +} + +static uint32_t rc_cd_find_file_sector(const rc_hash_iterator_t* iterator, void* track_handle, const char* path, uint32_t* size) +{ + uint8_t buffer[2048], *tmp; + int sector; + uint32_t 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) { + /* find the directory record for the first part of the path */ + memcpy(buffer, path, slash - path); + buffer[slash - path] = '\0'; + + sector = rc_cd_find_file_sector(iterator, track_handle, (const char *)buffer, NULL); + if (!sector) + return 0; + + ++slash; + filename_length -= (slash - path); + path = slash; + } + else { + uint32_t logical_block_size; + + /* find the cd information */ + if (!rc_cd_read_sector(iterator, track_handle, rc_cd_first_track_sector(iterator, track_handle) + 16, buffer, 256)) + return 0; + + /* the directory_record starts at 156, the sector containing the table of contents is 2 bytes into that. + * 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 */ + if (!rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer))) + return 0; + + tmp = buffer; + do { + 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(iterator, 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[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); + + rc_hash_iterator_verbose_formatted(iterator, "Found %s at sector %d", path, sector); + + if (size) + *size = tmp[10] | (tmp[11] << 8) | (tmp[12] << 16) | (tmp[13] << 24); + + return sector; + } + + /* the first byte of the record is the length of the record */ + tmp += *tmp; + } while (1); + + return 0; +} + +/* ===================================================== */ + +static int rc_hash_cd_file(md5_state_t* md5, const rc_hash_iterator_t* iterator, void* track_handle, uint32_t sector, const char* name, uint32_t size, const char* description) +{ + uint8_t buffer[2048]; + size_t num_read; + + if ((num_read = rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer))) < sizeof(buffer)) + return rc_hash_iterator_error_formatted(iterator, "Could not read %s", description); + + if (size > MAX_BUFFER_SIZE) + size = MAX_BUFFER_SIZE; + + if (name) + rc_hash_iterator_verbose_formatted(iterator, "Hashing %s title (%u bytes) and contents (%u bytes) ", name, (unsigned)strlen(name), size); + else + rc_hash_iterator_verbose_formatted(iterator, "Hashing %s contents (%u bytes @ sector %u)", description, size, sector); + + if (size < (unsigned)num_read) /* we read a whole sector - only hash the part containing file data */ + num_read = (size_t)size; + + do { + md5_append(md5, buffer, (int)num_read); + + if (size <= (unsigned)num_read) + break; + size -= (unsigned)num_read; + + ++sector; + if (size >= sizeof(buffer)) + num_read = rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + else + num_read = rc_cd_read_sector(iterator, track_handle, sector, buffer, size); + } while (num_read > 0); + + return 1; +} + +/* ===================================================== */ + +int rc_hash_3do(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[2048]; + const uint8_t operafs_identifier[7] = { 0x01, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x01 }; + void* track_handle; + md5_state_t md5; + int sector; + int block_size, block_location; + int offset, stop; + size_t size = 0; + + track_handle = rc_cd_open_track(iterator, 1); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + /* the Opera filesystem stores the volume information in the first 132 bytes of sector 0 + * https://github.com/barbeque/3dodump/blob/master/OperaFS-Format.md + */ + rc_cd_read_sector(iterator, track_handle, 0, buffer, 132); + + if (memcmp(buffer, operafs_identifier, sizeof(operafs_identifier)) == 0) { + rc_hash_iterator_verbose_formatted(iterator, "Found 3DO CD, title=%.32s", &buffer[0x28]); + + /* include the volume header in the hash */ + md5_init(&md5); + md5_append(&md5, buffer, 132); + + /* the block size is at offset 0x4C (assume 0x4C is always 0) */ + block_size = buffer[0x4D] * 65536 + buffer[0x4E] * 256 + buffer[0x4F]; + + /* the root directory block location is at offset 0x64 (and duplicated several + * times, but we just look at the primary record) (assume 0x64 is always 0)*/ + block_location = buffer[0x65] * 65536 + buffer[0x66] * 256 + buffer[0x67]; + + /* multiply the block index by the block size to get the real address */ + block_location *= block_size; + + /* convert that to a sector and read it */ + sector = block_location / 2048; + + do { + rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + + /* offset to start of entries is at offset 0x10 (assume 0x10 and 0x11 are always 0) */ + offset = buffer[0x12] * 256 + buffer[0x13]; + + /* offset to end of entries is at offset 0x0C (assume 0x0C is always 0) */ + stop = buffer[0x0D] * 65536 + buffer[0x0E] * 256 + buffer[0x0F]; + + while (offset < stop) { + if (buffer[offset + 0x03] == 0x02) { /* file */ + if (strcasecmp((const char*)&buffer[offset + 0x20], "LaunchMe") == 0) { + /* the block size is at offset 0x0C (assume 0x0C is always 0) */ + block_size = buffer[offset + 0x0D] * 65536 + buffer[offset + 0x0E] * 256 + buffer[offset + 0x0F]; + + /* the block location is at offset 0x44 (assume 0x44 is always 0) */ + block_location = buffer[offset + 0x45] * 65536 + buffer[offset + 0x46] * 256 + buffer[offset + 0x47]; + block_location *= block_size; + + /* the file size is at offset 0x10 (assume 0x10 is always 0) */ + size = (size_t)buffer[offset + 0x11] * 65536 + buffer[offset + 0x12] * 256 + buffer[offset + 0x13]; + + rc_hash_iterator_verbose_formatted(iterator, "Hashing header (%u bytes) and %.32s (%u bytes) ", 132, &buffer[offset + 0x20], (unsigned)size); + + break; + } + } + + /* the number of extra copies of the file is at offset 0x40 (assume 0x40-0x42 are always 0) */ + offset += 0x48 + buffer[offset + 0x43] * 4; + } + + if (size != 0) + break; + + /* did not find the file, see if the directory listing is continued in another sector */ + offset = buffer[0x02] * 256 + buffer[0x03]; + + /* no more sectors to search*/ + if (offset == 0xFFFF) + break; + + /* get next sector */ + offset *= block_size; + sector = (block_location + offset) / 2048; + } while (1); + + if (size == 0) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Could not find LaunchMe"); + } + + sector = block_location / 2048; + + while (size > 2048) { + rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + size -= 2048; + } + + rc_cd_read_sector(iterator, track_handle, sector, buffer, size); + md5_append(&md5, buffer, (int)size); + } + else { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Not a 3DO CD"); + } + + rc_cd_close_track(iterator, track_handle); + + return rc_hash_finalize(iterator, &md5, hash); +} + +int rc_hash_dreamcast(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[256] = ""; + void* track_handle; + char exe_file[32] = ""; + uint32_t size; + uint32_t sector; + int result = 0; + md5_state_t md5; + int i = 0; + + /* track 03 is the data track that contains the TOC and IP.BIN */ + track_handle = rc_cd_open_track(iterator, 3); + if (track_handle) { + /* first 256 bytes from first sector should have IP.BIN structure that stores game meta information + * https://mc.pp.se/dc/ip.bin.html */ + rc_cd_read_sector(iterator, track_handle, rc_cd_first_track_sector(iterator, track_handle), buffer, sizeof(buffer)); + } + + if (memcmp(&buffer[0], "SEGA SEGAKATANA ", 16) != 0) { + if (track_handle) + rc_cd_close_track(iterator, track_handle); + + /* not a gd-rom dreamcast file. check for mil-cd by looking for the marker in the first data track */ + track_handle = rc_cd_open_track(iterator, RC_HASH_CDTRACK_FIRST_DATA); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + rc_cd_read_sector(iterator, track_handle, rc_cd_first_track_sector(iterator, track_handle), buffer, sizeof(buffer)); + if (memcmp(&buffer[0], "SEGA SEGAKATANA ", 16) != 0) { + /* did not find marker on track 3 or first data track */ + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Not a Dreamcast CD"); + } + } + + /* start the hash with the game meta information */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)buffer, 256); + + if (iterator->callbacks.verbose_message) { + uint8_t* ptr = &buffer[0xFF]; + while (ptr > &buffer[0x80] && ptr[-1] == ' ') + --ptr; + *ptr = '\0'; + + rc_hash_iterator_verbose_formatted(iterator, "Found Dreamcast CD: %.128s (%.16s)", (const char*)&buffer[0x80], (const char*)&buffer[0x40]); + } + + /* the boot filename is 96 bytes into the meta information (https://mc.pp.se/dc/ip0000.bin.html) */ + /* remove whitespace from bootfile */ + i = 0; + while (!isspace((unsigned char)buffer[96 + i]) && i < 16) + ++i; + + /* sometimes boot file isn't present on meta information. + * nothing can be done, as even the core doesn't run the game in this case. */ + if (i == 0) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Boot executable not specified on IP.BIN"); + } + + memcpy(exe_file, &buffer[96], i); + exe_file[i] = '\0'; + + sector = rc_cd_find_file_sector(iterator, track_handle, exe_file, &size); + if (sector == 0) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Could not locate boot executable"); + } + + if (rc_cd_read_sector(iterator, track_handle, sector, buffer, 1)) { + /* the boot executable is in the primary data track */ + } + else { + rc_cd_close_track(iterator, track_handle); + + /* the boot executable is normally in the last track */ + track_handle = rc_cd_open_track(iterator, RC_HASH_CDTRACK_LAST); + } + + result = rc_hash_cd_file(&md5, iterator, track_handle, sector, NULL, size, "boot executable"); + rc_cd_close_track(iterator, track_handle); + + rc_hash_finalize(iterator, &md5, hash); + return result; +} + +static int rc_hash_nintendo_disc_partition(md5_state_t* md5, const rc_hash_iterator_t* iterator, + void* file_handle, const uint32_t part_offset, uint8_t wii_shift) +{ + 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; + + uint64_t dol_offset; + uint64_t dol_offsets[18]; + uint64_t dol_sizes[18]; + + uint8_t ix; + uint64_t remaining_size; + const uint32_t MAX_CHUNK_SIZE = 1024 * 1024; + + /* GetApploaderSize */ + rc_file_seek(iterator, file_handle, part_offset + BASE_HEADER_SIZE + 0x14, SEEK_SET); + apploader_header_size = 0x20; + rc_file_read(iterator, 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(iterator, 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(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Could not allocate temporary buffer"); + } + rc_file_seek(iterator, file_handle, part_offset, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, header_size); + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte partition header", header_size); + md5_append(md5, buffer, header_size); + + /* GetBootDOLOffset + * Base header size is guaranteed larger than 0x423 therefore buffer contains dol_offset right now + */ + dol_offset = (((uint64_t)buffer[0x420] << 24) | + ((uint64_t)buffer[0x421] << 16) | + ((uint64_t)buffer[0x422] << 8) | + (uint64_t)buffer[0x423]) << wii_shift; + free(buffer); + + /* Find offsets and sizes for the 7 main.dol code segments and 11 main.dol data segments */ + rc_file_seek(iterator, file_handle, part_offset + dol_offset, SEEK_SET); + rc_file_read(iterator, file_handle, addr_buffer, 0xD8); + for (ix = 0; ix < 18; ix++) { + dol_offsets[ix] = (((uint64_t)addr_buffer[0x0 + ix * 4] << 24) | + ((uint64_t)addr_buffer[0x1 + ix * 4] << 16) | + ((uint64_t)addr_buffer[0x2 + ix * 4] << 8) | + (uint64_t)addr_buffer[0x3 + ix * 4]) << wii_shift; + dol_sizes[ix] = (((uint64_t)addr_buffer[0x90 + ix * 4] << 24) | + ((uint64_t)addr_buffer[0x91 + ix * 4] << 16) | + ((uint64_t)addr_buffer[0x92 + ix * 4] << 8) | + (uint64_t)addr_buffer[0x93 + ix * 4]) << wii_shift; + } + + /* Iterate through the 18 main.dol segments and hash each */ + buffer = (uint8_t*)malloc(MAX_CHUNK_SIZE); + if (!buffer) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Could not allocate temporary buffer"); + } + + for (ix = 0; ix < 18; ix++) { + if (dol_sizes[ix] == 0) + continue; + + rc_file_seek(iterator, file_handle, part_offset + dol_offsets[ix], SEEK_SET); + if (ix < 7) + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte main.dol code segment %u", dol_sizes[ix], ix); + else + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte main.dol data segment %u", dol_sizes[ix], ix - 7); + + remaining_size = dol_sizes[ix]; + while (remaining_size > MAX_CHUNK_SIZE) { + rc_file_read(iterator, file_handle, buffer, MAX_CHUNK_SIZE); + md5_append(md5, buffer, MAX_CHUNK_SIZE); + remaining_size -= MAX_CHUNK_SIZE; + } + rc_file_read(iterator, file_handle, buffer, (int32_t)remaining_size); + md5_append(md5, buffer, (int32_t)remaining_size); + } + + free(buffer); + return 1; +} + +int rc_hash_gamecube(char hash[33], const rc_hash_iterator_t* iterator) +{ + md5_state_t md5; + void* file_handle; + + uint8_t quad_buffer[4]; + uint8_t success; + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + md5_init(&md5); + /* Check Magic Word */ + rc_file_seek(iterator, file_handle, 0x1c, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + if (quad_buffer[0] == 0xC2 && quad_buffer[1] == 0x33 && quad_buffer[2] == 0x9F && quad_buffer[3] == 0x3D) + success = rc_hash_nintendo_disc_partition(&md5, iterator, file_handle, 0, 0); + else + success = rc_hash_iterator_error(iterator, "Not a Gamecube disc"); + + /* Finalize */ + rc_file_close(iterator, file_handle); + + if (success) + return rc_hash_finalize(iterator, &md5, hash); + + return 0; +} + +/* helper variable only used for testing */ +const char* _rc_hash_jaguar_cd_homebrew_hash = NULL; + +int rc_hash_jaguar_cd(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[2352]; + void* track_handle; + md5_state_t md5; + int byteswapped = 0; + uint32_t size = 0; + uint32_t offset = 0; + uint32_t sector = 0; + uint32_t remaining; + uint32_t 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(iterator, RC_HASH_CDTRACK_FIRST_OF_SECOND_SESSION); + if (!track_handle) + return rc_hash_iterator_error(iterator, "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(iterator, track_handle); + rc_cd_read_sector(iterator, 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(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Not a Jaguar CD"); + } + + i = 0; /* only loop once */ + do { + md5_init(&md5); + + offset += 4; + + rc_hash_iterator_verbose_formatted(iterator, "Hashing boot executable (%u bytes starting at %u bytes into sector %u)", size, offset, sector); + + 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(iterator, track_handle, ++sector, buffer, sizeof(buffer)) == sizeof(buffer)); + + rc_cd_close_track(iterator, track_handle); + + if (size > 0) + return rc_hash_iterator_error(iterator, "Not enough data"); + + rc_hash_finalize(iterator, &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; + + rc_hash_iterator_verbose_formatted(iterator, "Potential homebrew at sector %u, checking for KART data in track 2", sector); + + track_handle = rc_cd_open_track(iterator, 2); + if (!track_handle) + return rc_hash_iterator_error(iterator, "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(iterator, track_handle); + rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + if (memcmp(&buffer[0x5E], "RT!IRTKA", 8) != 0) + return rc_hash_iterator_error(iterator, "Homebrew executable not found in track 2"); + + /* found KART data*/ + rc_hash_iterator_verbose(iterator, "Found KART data in track 2"); + + offset = 0xA6; + size = (buffer[offset] << 16) | (buffer[offset + 1] << 24) | (buffer[offset + 2]) | (buffer[offset + 3] << 8); + } while (1); +} + +int rc_hash_neogeo_cd(char hash[33], const rc_hash_iterator_t* iterator) +{ + char buffer[1024], *ptr; + void* track_handle; + uint32_t sector; + uint32_t size; + md5_state_t md5; + + track_handle = rc_cd_open_track(iterator, 1); + if (!track_handle) + return rc_hash_iterator_error(iterator, "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(iterator, track_handle, "IPL.TXT", &size); + if (!sector) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Not a NeoGeo CD game disc"); + } + + if (rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)) == 0) { + rc_cd_close_track(iterator, 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(iterator, track_handle, start, &size); + if (!sector || !rc_hash_cd_file(&md5, iterator, track_handle, sector, NULL, size, start)) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error_formatted(iterator, "Could not read %.16s", start); + } + } + + while (*ptr && *ptr != '\n') + ++ptr; + if (*ptr != '\n') + break; + ++ptr; + } while (*ptr != '\0' && *ptr != '\x1a'); + + rc_cd_close_track(iterator, track_handle); + return rc_hash_finalize(iterator, &md5, hash); +} + +static int rc_hash_pce_track(char hash[33], void* track_handle, const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[2048]; + md5_state_t md5; + uint32_t sector, num_sectors; + uint32_t size; + + /* the PC-Engine uses the second sector to specify boot information and program name. + * the string "PC Engine CD-ROM SYSTEM" should exist at 32 bytes into the sector + * http://shu.sheldows.com/shu/download/pcedocs/pce_cdrom.html + */ + if (rc_cd_read_sector(iterator, track_handle, rc_cd_first_track_sector(iterator, track_handle) + 1, buffer, 128) < 128) + return rc_hash_iterator_error(iterator, "Not a PC Engine CD"); + + /* normal PC Engine CD will have a header block in sector 1 */ + if (memcmp("PC Engine CD-ROM SYSTEM", &buffer[32], 23) == 0) { + /* the title of the disc is the last 22 bytes of the header */ + md5_init(&md5); + md5_append(&md5, &buffer[106], 22); + + buffer[128] = '\0'; + rc_hash_iterator_verbose_formatted(iterator, "Found PC Engine CD, title=%.22s", &buffer[106]); + + /* the first three bytes specify the sector of the program data, and the fourth byte + * is the number of sectors. + */ + sector = (buffer[0] << 16) + (buffer[1] << 8) + buffer[2]; + num_sectors = buffer[3]; + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %d sectors starting at sector %d", num_sectors, sector); + + sector += rc_cd_first_track_sector(iterator, track_handle); + while (num_sectors > 0) { + rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + --num_sectors; + } + } + /* GameExpress CDs use a standard Joliet filesystem - locate and hash the BOOT.BIN */ + else if ((sector = rc_cd_find_file_sector(iterator, track_handle, "BOOT.BIN", &size)) != 0 && size < MAX_BUFFER_SIZE) { + md5_init(&md5); + while (size > sizeof(buffer)) { + rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + size -= sizeof(buffer); + } + + if (size > 0) { + rc_cd_read_sector(iterator, track_handle, sector, buffer, size); + md5_append(&md5, buffer, size); + } + } + else { + return rc_hash_iterator_error(iterator, "Not a PC Engine CD"); + } + + return rc_hash_finalize(iterator, &md5, hash); +} + +int rc_hash_pce_cd(char hash[33], const rc_hash_iterator_t* iterator) +{ + int result; + void* track_handle = rc_cd_open_track(iterator, RC_HASH_CDTRACK_FIRST_DATA); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + result = rc_hash_pce_track(hash, track_handle, iterator); + + rc_cd_close_track(iterator, track_handle); + + return result; +} + +int rc_hash_pcfx_cd(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[2048]; + void* track_handle; + md5_state_t md5; + int sector, num_sectors; + + /* PC-FX executable can be in any track. Assume it's in the largest data track and check there first */ + track_handle = rc_cd_open_track(iterator, RC_HASH_CDTRACK_LARGEST); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + /* PC-FX CD will have a header marker in sector 0 */ + sector = rc_cd_first_track_sector(iterator, track_handle); + rc_cd_read_sector(iterator, track_handle, sector, buffer, 32); + if (memcmp("PC-FX:Hu_CD-ROM", &buffer[0], 15) != 0) { + rc_cd_close_track(iterator, track_handle); + + /* not found in the largest data track, check track 2 */ + track_handle = rc_cd_open_track(iterator, 2); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + sector = rc_cd_first_track_sector(iterator, track_handle); + rc_cd_read_sector(iterator, track_handle, sector, buffer, 32); + } + + if (memcmp("PC-FX:Hu_CD-ROM", &buffer[0], 15) == 0) { + /* PC-FX boot header fills the first two sectors of the disc + * https://bitbucket.org/trap15/pcfxtools/src/master/pcfx-cdlink.c + * the important stuff is the first 128 bytes of the second sector (title being the first 32) */ + rc_cd_read_sector(iterator, track_handle, sector + 1, buffer, 128); + + md5_init(&md5); + md5_append(&md5, buffer, 128); + + rc_hash_iterator_verbose_formatted(iterator, "Found PC-FX CD, title=%.32s", &buffer[0]); + + /* the program sector is in bytes 33-36 (assume byte 36 is 0) */ + sector = (buffer[34] << 16) + (buffer[33] << 8) + buffer[32]; + + /* the number of sectors the program occupies is in bytes 37-40 (assume byte 40 is 0) */ + num_sectors = (buffer[38] << 16) + (buffer[37] << 8) + buffer[36]; + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %d sectors starting at sector %d", num_sectors, sector); + + sector += rc_cd_first_track_sector(iterator, track_handle); + while (num_sectors > 0) { + rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)); + md5_append(&md5, buffer, sizeof(buffer)); + + ++sector; + --num_sectors; + } + } + else { + int result = 0; + rc_cd_read_sector(iterator, track_handle, sector + 1, buffer, 128); + + /* some PC-FX CDs still identify as PCE CDs */ + if (memcmp("PC Engine CD-ROM SYSTEM", &buffer[32], 23) == 0) + result = rc_hash_pce_track(hash, track_handle, iterator); + + rc_cd_close_track(iterator, track_handle); + if (result) + return result; + + return rc_hash_iterator_error(iterator, "Not a PC-FX CD"); + } + + rc_cd_close_track(iterator, track_handle); + + return rc_hash_finalize(iterator, &md5, hash); +} + +static int rc_hash_find_playstation_executable(const rc_hash_iterator_t* iterator, void* track_handle, + const char* boot_key, const char* cdrom_prefix, + char exe_name[], uint32_t exe_name_size, uint32_t* exe_size) +{ + uint8_t buffer[2048]; + uint32_t size; + char* ptr; + char* start; + const size_t boot_key_len = strlen(boot_key); + const size_t cdrom_prefix_len = strlen(cdrom_prefix); + int sector; + + sector = rc_cd_find_file_sector(iterator, track_handle, "SYSTEM.CNF", NULL); + if (!sector) + return 0; + + size = (uint32_t)rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer) - 1); + buffer[size] = '\0'; + + sector = 0; + for (ptr = (char*)buffer; *ptr; ++ptr) { + if (strncmp(ptr, boot_key, boot_key_len) == 0) { + ptr += boot_key_len; + while (isspace((unsigned char)*ptr)) + ++ptr; + + if (*ptr == '=') { + ++ptr; + while (isspace((unsigned char)*ptr)) + ++ptr; + + if (strncmp(ptr, cdrom_prefix, cdrom_prefix_len) == 0) + ptr += cdrom_prefix_len; + while (*ptr == '\\') + ++ptr; + + start = ptr; + while (!isspace((unsigned char)*ptr) && *ptr != ';') + ++ptr; + + size = (uint32_t)(ptr - start); + if (size >= exe_name_size) + size = exe_name_size - 1; + + memcpy(exe_name, start, size); + exe_name[size] = '\0'; + + rc_hash_iterator_verbose_formatted(iterator, "Looking for boot executable: %s", exe_name); + + sector = rc_cd_find_file_sector(iterator, track_handle, exe_name, exe_size); + break; + } + } + + /* advance to end of line */ + while (*ptr && *ptr != '\n') + ++ptr; + } + + return sector; +} + +int rc_hash_psx(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[32]; + char exe_name[64] = ""; + void* track_handle; + uint32_t sector; + uint32_t size; + int result = 0; + md5_state_t md5; + + track_handle = rc_cd_open_track(iterator, 1); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + sector = rc_hash_find_playstation_executable(iterator, track_handle, "BOOT", "cdrom:", exe_name, sizeof(exe_name), &size); + if (!sector) { + sector = rc_cd_find_file_sector(iterator, track_handle, "PSX.EXE", &size); + if (sector) + memcpy(exe_name, "PSX.EXE", 8); + } + + if (!sector) { + rc_hash_iterator_error(iterator, "Could not locate primary executable"); + } + else if (rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)) < sizeof(buffer)) { + rc_hash_iterator_error(iterator, "Could not read primary executable"); + } + else { + if (memcmp(buffer, "PS-X EXE", 7) != 0) { + rc_hash_iterator_verbose_formatted(iterator, "%s did not contain PS-X EXE marker", exe_name); + } + else { + /* the PS-X EXE header specifies the executable size as a 4-byte value 28 bytes into the header, which doesn't + * include the header itself. We want to include the header in the hash, so append another 2048 to that value. + */ + size = (((uint8_t)buffer[31] << 24) | ((uint8_t)buffer[30] << 16) | ((uint8_t)buffer[29] << 8) | (uint8_t)buffer[28]) + 2048; + } + + /* there's a few games that use a singular engine and only differ via their data files. luckily, they have unique + * serial numbers, and use the serial number as the boot file in the standard way. include the boot file name in the hash. + */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)exe_name, (int)strlen(exe_name)); + + result = rc_hash_cd_file(&md5, iterator, track_handle, sector, exe_name, size, "primary executable"); + rc_hash_finalize(iterator, &md5, hash); + } + + rc_cd_close_track(iterator, track_handle); + + return result; +} + +int rc_hash_ps2(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[4]; + char exe_name[64] = ""; + void* track_handle; + uint32_t sector; + uint32_t size; + int result = 0; + md5_state_t md5; + + track_handle = rc_cd_open_track(iterator, 1); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + sector = rc_hash_find_playstation_executable(iterator, track_handle, "BOOT2", "cdrom0:", exe_name, sizeof(exe_name), &size); + if (!sector) { + rc_hash_iterator_error(iterator, "Could not locate primary executable"); + } + else if (rc_cd_read_sector(iterator, track_handle, sector, buffer, sizeof(buffer)) < sizeof(buffer)) { + rc_hash_iterator_error(iterator, "Could not read primary executable"); + } + else { + if (memcmp(buffer, "\x7f\x45\x4c\x46", 4) != 0) + rc_hash_iterator_verbose_formatted(iterator, "%s did not contain ELF marker", exe_name); + + /* there's a few games that use a singular engine and only differ via their data files. luckily, they have unique + * serial numbers, and use the serial number as the boot file in the standard way. include the boot file name in the hash. + */ + md5_init(&md5); + md5_append(&md5, (md5_byte_t*)exe_name, (int)strlen(exe_name)); + + result = rc_hash_cd_file(&md5, iterator, track_handle, sector, exe_name, size, "primary executable"); + rc_hash_finalize(iterator, &md5, hash); + } + + rc_cd_close_track(iterator, track_handle); + + return result; +} + +int rc_hash_psp(char hash[33], const rc_hash_iterator_t* iterator) +{ + void* track_handle; + uint32_t sector; + uint32_t size; + md5_state_t md5; + + /* https://www.psdevwiki.com/psp/PBP + * A PBP file is an archive containing the PARAM.SFO, primary executable, and a bunch of metadata. + * While we could extract the PARAM.SFO and primary executable to mimic the normal PSP hashing logic, + * it's easier to just hash the entire file. This also helps alleviate issues where the primary + * executable is just a game engine and the only differentiating data would be the metadata. */ + if (rc_path_compare_extension(iterator->path, "pbp")) + return rc_hash_whole_file(hash, iterator); + + track_handle = rc_cd_open_track(iterator, 1); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + /* http://www.romhacking.net/forum/index.php?topic=30899.0 + * PSP_GAME/PARAM.SFO contains key/value pairs identifying the game for the system (i.e. serial number, + * name, version). PSP_GAME/SYSDIR/EBOOT.BIN is the encrypted primary executable. + */ + sector = rc_cd_find_file_sector(iterator, track_handle, "PSP_GAME\\PARAM.SFO", &size); + if (!sector) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Not a PSP game disc"); + } + + md5_init(&md5); + if (!rc_hash_cd_file(&md5, iterator, track_handle, sector, NULL, size, "PSP_GAME\\PARAM.SFO")) { + rc_cd_close_track(iterator, track_handle); + return 0; + } + + sector = rc_cd_find_file_sector(iterator, track_handle, "PSP_GAME\\SYSDIR\\EBOOT.BIN", &size); + if (!sector) { + rc_cd_close_track(iterator, track_handle); + return rc_hash_iterator_error(iterator, "Could not find primary executable"); + } + + if (!rc_hash_cd_file(&md5, iterator, track_handle, sector, NULL, size, "PSP_GAME\\SYSDIR\\EBOOT.BIN")) { + rc_cd_close_track(iterator, track_handle); + return 0; + } + + rc_cd_close_track(iterator, track_handle); + return rc_hash_finalize(iterator, &md5, hash); +} + +int rc_hash_sega_cd(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t buffer[512]; + void* track_handle; + + track_handle = rc_cd_open_track(iterator, 1); + if (!track_handle) + return rc_hash_iterator_error(iterator, "Could not open track"); + + /* the first 512 bytes of sector 0 are a volume header and ROM header that uniquely identify the game. + * After that is an arbitrary amount of code that ensures the game is being run in the correct region. + * Then more arbitrary code follows that actually starts the boot process. Somewhere in there, the + * primary executable is loaded. In many cases, a single game will have multiple executables, so even + * if we could determine the primary one, it's just the tip of the iceberg. As such, we've decided that + * hashing the volume and ROM headers is sufficient for identifying the game, and we'll have to trust + * that our players aren't modifying anything else on the disc. + */ + rc_cd_read_sector(iterator, track_handle, 0, buffer, sizeof(buffer)); + rc_cd_close_track(iterator, track_handle); + + if (memcmp(buffer, "SEGADISCSYSTEM ", 16) != 0 && /* Sega CD */ + memcmp(buffer, "SEGA SEGASATURN ", 16) != 0) { /* Sega Saturn */ + return rc_hash_iterator_error(iterator, "Not a Sega CD"); + } + + return rc_hash_buffer(hash, buffer, sizeof(buffer), iterator); +} + +static int rc_hash_wii_disc(md5_state_t* md5, const rc_hash_iterator_t* iterator, void* file_handle) +{ + const uint32_t MAIN_HEADER_SIZE = 0x80; + const uint64_t REGION_CODE_ADDRESS = 0x4E000; + const uint32_t CLUSTER_SIZE = 0x7C00; + const uint32_t MAX_CLUSTER_COUNT = 1024; + + uint32_t partition_info_table[8]; + uint32_t total_partition_count = 0; + uint32_t* partition_table; + uint64_t tmd_offset; + uint32_t tmd_size; + uint64_t part_offset; + uint64_t part_size; + uint32_t cluster_count; + + uint8_t quad_buffer[4]; + uint8_t* buffer; + + uint32_t ix, jx, kx; + uint8_t encrypted; + + /* Check encryption byte - if 0x61 is 0, disc is encrypted */ + rc_file_seek(iterator, file_handle, 0x61, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 1); + encrypted = (quad_buffer[0] == 0); + + /* Hash main headers */ + buffer = (uint8_t*)malloc(CLUSTER_SIZE); + if (!buffer) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Could not allocate temporary buffer"); + } + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte main header for [%c%c%c%c%c%c]", + MAIN_HEADER_SIZE, buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5]); + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, MAIN_HEADER_SIZE); + md5_append(md5, buffer, MAIN_HEADER_SIZE); + + /* Hash region code */ + rc_file_seek(iterator, file_handle, REGION_CODE_ADDRESS, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + md5_append(md5, quad_buffer, 4); + + /* Scan partition table */ + rc_file_seek(iterator, file_handle, 0x40000, SEEK_SET); + for (ix = 0; ix < 8; ix++) { + rc_file_read(iterator, file_handle, quad_buffer, 4); + partition_info_table[ix] = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + if (ix % 2 == 0) + total_partition_count += partition_info_table[ix]; + } + + if (total_partition_count == 0) { + free(buffer); + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "No partitions found"); + } + + partition_table = (uint32_t*)malloc(total_partition_count * 4 * 2); + kx = 0; + for (jx = 0; jx < 8; jx += 2) { + rc_file_seek(iterator, file_handle, ((uint64_t)partition_info_table[jx + 1]) << 2, SEEK_SET); + for (ix = 0; ix < partition_info_table[jx]; ix++) { + rc_file_read(iterator, file_handle, quad_buffer, 4); + partition_table[kx++] = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + rc_file_read(iterator, file_handle, quad_buffer, 4); + partition_table[kx++] = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + } + } + + /* Read each partition */ + for (jx = 0; jx < total_partition_count * 2; jx += 2) { + /* Don't hash Update partition*/ + if (partition_table[jx + 1] == 1) + continue; + + /* Hash title metadata */ + rc_file_seek(iterator, file_handle, ((uint64_t)partition_table[jx] << 2) + 0x2A4, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + tmd_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + rc_file_read(iterator, file_handle, quad_buffer, 4); + tmd_offset = + ((uint64_t)((quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3])) << 2; + + if (tmd_size > CLUSTER_SIZE) + tmd_size = CLUSTER_SIZE; + + rc_file_seek(iterator, file_handle, ((uint64_t)partition_table[jx] << 2) + tmd_offset, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, tmd_size); + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte title metadata (partition type %u)", + tmd_size, partition_table[jx + 1]); + md5_append(md5, buffer, tmd_size); + + /* Hash partition */ + rc_file_seek(iterator, file_handle, ((uint64_t)partition_table[jx] << 2) + 0x2B8, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + part_offset = + ((uint64_t)((quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3])) << 2; + rc_file_read(iterator, file_handle, quad_buffer, 4); + part_size = + ((uint64_t)((quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3])) << 2; + + if (encrypted) { + cluster_count = (part_size / 0x8000 > MAX_CLUSTER_COUNT) ? MAX_CLUSTER_COUNT : (uint32_t)(part_size / 0x8000); + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u encrypted clusters (%u bytes)", + cluster_count, cluster_count * CLUSTER_SIZE); + for (ix = 0; ix < cluster_count; ix++) { + rc_file_seek(iterator, file_handle, part_offset + (ix * 0x8000) + 0x400, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, CLUSTER_SIZE); + md5_append(md5, buffer, CLUSTER_SIZE); + } + } + else { /* Decrypted */ + if (rc_hash_nintendo_disc_partition(md5, iterator, file_handle, (uint32_t)part_offset, 2) == 0) { + free(partition_table); + free(buffer); + return rc_hash_iterator_error(iterator, "Failed to hash Wii partition"); + } + } + } + free(partition_table); + free(buffer); + return 1; +} + +static int rc_hash_wiiware(md5_state_t* md5, const rc_hash_iterator_t* iterator, void* file_handle) +{ + uint32_t cert_chain_size, ticket_size, tmd_size; + uint32_t tmd_start_addr, content_count, content_addr, content_size, buffer_size; + uint32_t ix; + + uint8_t quad_buffer[4]; + uint8_t* buffer; + + rc_file_seek(iterator, file_handle, 0x08, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + cert_chain_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + /* Each content is individually aligned to a 0x40-byte boundary. */ + cert_chain_size = (cert_chain_size + 0x3F) & ~0x3F; + rc_file_seek(iterator, file_handle, 0x10, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + ticket_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + ticket_size = (ticket_size + 0x3F) & ~0x3F; + rc_file_read(iterator, file_handle, quad_buffer, 4); + tmd_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + tmd_size = (tmd_size + 0x3F) & ~0x3F; + if (tmd_size > MAX_BUFFER_SIZE) + tmd_size = MAX_BUFFER_SIZE; + + tmd_start_addr = 0x40 + cert_chain_size + ticket_size; + + /* Hash TMD */ + buffer = (uint8_t*)malloc(tmd_size); + rc_file_seek(iterator, file_handle, tmd_start_addr, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, tmd_size); + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte TMD", tmd_size); + md5_append(md5, buffer, tmd_size); + free(buffer); + + /* Get count of content sections */ + rc_file_seek(iterator, file_handle, (uint64_t)tmd_start_addr + 0x1de, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 2); + content_count = (quad_buffer[0] << 8) | quad_buffer[1]; + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u content sections", content_count); + content_addr = tmd_start_addr + tmd_size; + for (ix = 0; ix < content_count; ix++) { + /* Get content section size */ + rc_file_seek(iterator, file_handle, (uint64_t)tmd_start_addr + 0x1e4 + 8 + ix * 0x24, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + if (quad_buffer[0] == 0x00 && quad_buffer[1] == 0x00 && quad_buffer[2] == 0x00 && quad_buffer[3] == 0x00) { + rc_file_read(iterator, file_handle, quad_buffer, 4); + content_size = + (quad_buffer[0] << 24) | (quad_buffer[1] << 16) | (quad_buffer[2] << 8) | quad_buffer[3]; + /* Padding between content should be ignored. But because the content data is encrypted, + the size to hash for each content should be rounded up to the size of an AES block (16 bytes). */ + content_size = (content_size + 0x0F) & ~0x0F; + } + else { + /* size > 4GB, just assume MAX_BUFFER_SIZE */ + content_size = MAX_BUFFER_SIZE; + } + buffer_size = (content_size > MAX_BUFFER_SIZE) ? MAX_BUFFER_SIZE : content_size; + + /* Hash content */ + buffer = (uint8_t*)malloc(buffer_size); + rc_file_seek(iterator, file_handle, content_addr, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, buffer_size); + md5_append(md5, buffer, buffer_size); + content_addr += content_size; + content_addr = (content_addr + 0x3F) & ~0x3F; + free(buffer); + } + + return 1; +} + +int rc_hash_wii(char hash[33], const rc_hash_iterator_t* iterator) +{ + md5_state_t md5; + void* file_handle; + + uint8_t quad_buffer[4]; + uint8_t success; + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + md5_init(&md5); + /* Check Magic Words */ + rc_file_seek(iterator, file_handle, 0x18, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + if (quad_buffer[0] == 0x5D && quad_buffer[1] == 0x1C && quad_buffer[2] == 0x9E && quad_buffer[3] == 0xA3) { + success = rc_hash_wii_disc(&md5, iterator, file_handle); + } + else { + rc_file_seek(iterator, file_handle, 0x04, SEEK_SET); + rc_file_read(iterator, file_handle, quad_buffer, 4); + if (quad_buffer[0] == 'I' && quad_buffer[1] == 's' && quad_buffer[2] == 0x00 && quad_buffer[3] == 0x00) + success = rc_hash_wiiware(&md5, iterator, file_handle); + else + success = rc_hash_iterator_error(iterator, "Not a supported Wii file"); + } + + /* Finalize */ + rc_file_close(iterator, file_handle); + + if (success) + return rc_hash_finalize(iterator, &md5, hash); + + return 0; +} diff --git a/deps/rcheevos/src/rhash/hash_encrypted.c b/deps/rcheevos/src/rhash/hash_encrypted.c new file mode 100644 index 0000000000..e90e310757 --- /dev/null +++ b/deps/rcheevos/src/rhash/hash_encrypted.c @@ -0,0 +1,566 @@ +#include "rc_hash_internal.h" + +#include "../rc_compat.h" + +#include "aes.h" + +/* ===================================================== */ + +static rc_hash_3ds_get_cia_normal_key_func _3ds_get_cia_normal_key_func = NULL; +static rc_hash_3ds_get_ncch_normal_keys_func _3ds_get_ncch_normal_keys_func = NULL; + +void rc_hash_reset_iterator_encrypted(rc_hash_iterator_t* iterator) +{ + iterator->callbacks.encryption.get_3ds_cia_normal_key = _3ds_get_cia_normal_key_func; + iterator->callbacks.encryption.get_3ds_ncch_normal_keys = _3ds_get_ncch_normal_keys_func; +} + +void rc_hash_init_3ds_get_cia_normal_key_func(rc_hash_3ds_get_cia_normal_key_func func) +{ + _3ds_get_cia_normal_key_func = func; +} + +void rc_hash_init_3ds_get_ncch_normal_keys_func(rc_hash_3ds_get_ncch_normal_keys_func func) +{ + _3ds_get_ncch_normal_keys_func = func; +} + +/* ===================================================== */ + +static int rc_hash_nintendo_3ds_ncch(md5_state_t* md5, void* file_handle, uint8_t header[0x200], + struct AES_ctx* cia_aes, const rc_hash_iterator_t* iterator) +{ + struct AES_ctx ncch_aes; + uint8_t* hash_buffer; + uint64_t exefs_offset, exefs_real_size; + uint32_t exefs_buffer_size; + uint8_t primary_key[AES_KEYLEN], secondary_key[AES_KEYLEN]; + uint8_t fixed_key_flag, no_crypto_flag, seed_crypto_flag; + uint8_t crypto_method, secondary_key_x_slot; + uint16_t ncch_version; + uint32_t i; + uint8_t primary_key_y[AES_KEYLEN], program_id[sizeof(uint64_t)]; + uint8_t iv[AES_BLOCKLEN], cia_iv[AES_BLOCKLEN]; + uint8_t exefs_section_name[8]; + uint64_t exefs_section_offset, exefs_section_size; + + exefs_offset = ((uint32_t)header[0x1A3] << 24) | (header[0x1A2] << 16) | (header[0x1A1] << 8) | header[0x1A0]; + exefs_real_size = ((uint32_t)header[0x1A7] << 24) | (header[0x1A6] << 16) | (header[0x1A5] << 8) | header[0x1A4]; + + /* Offset and size are in "media units" (1 media unit = 0x200 bytes) */ + exefs_offset *= 0x200; + exefs_real_size *= 0x200; + + if (exefs_real_size > MAX_BUFFER_SIZE) + exefs_buffer_size = MAX_BUFFER_SIZE; + else + exefs_buffer_size = (uint32_t)exefs_real_size; + + /* This region is technically optional, but it should always be present for executable content (i.e. games) */ + if (exefs_offset == 0 || exefs_real_size == 0) + return rc_hash_iterator_error(iterator, "ExeFS was not available"); + + /* NCCH flag 7 is a bitfield of various crypto related flags */ + fixed_key_flag = header[0x188 + 7] & 0x01; + no_crypto_flag = header[0x188 + 7] & 0x04; + seed_crypto_flag = header[0x188 + 7] & 0x20; + + ncch_version = (header[0x113] << 8) | header[0x112]; + + if (no_crypto_flag == 0) { + rc_hash_iterator_verbose(iterator, "Encrypted NCCH detected"); + + if (fixed_key_flag != 0) { + /* Fixed crypto key means all 0s for both keys */ + memset(primary_key, 0, sizeof(primary_key)); + memset(secondary_key, 0, sizeof(secondary_key)); + rc_hash_iterator_verbose(iterator, "Using fixed key crypto"); + } + else { + if (iterator->callbacks.encryption.get_3ds_ncch_normal_keys == NULL) + return rc_hash_iterator_error(iterator, "An encrypted NCCH was detected, but the NCCH normal keys callback was not set"); + + /* Primary key y is just the first 16 bytes of the header */ + memcpy(primary_key_y, header, sizeof(primary_key_y)); + + /* NCCH flag 3 indicates which secondary key x slot is used */ + crypto_method = header[0x188 + 3]; + + switch (crypto_method) { + case 0x00: + rc_hash_iterator_verbose(iterator, "Using NCCH crypto method v1"); + secondary_key_x_slot = 0x2C; + break; + case 0x01: + rc_hash_iterator_verbose(iterator, "Using NCCH crypto method v2"); + secondary_key_x_slot = 0x25; + break; + case 0x0A: + rc_hash_iterator_verbose(iterator, "Using NCCH crypto method v3"); + secondary_key_x_slot = 0x18; + break; + case 0x0B: + rc_hash_iterator_verbose(iterator, "Using NCCH crypto method v4"); + secondary_key_x_slot = 0x1B; + break; + default: + return rc_hash_iterator_error_formatted(iterator, "Invalid crypto method %02X", (unsigned)crypto_method); + } + + /* We only need the program id if we're doing seed crypto */ + if (seed_crypto_flag != 0) { + rc_hash_iterator_verbose(iterator, "Using seed crypto"); + memcpy(program_id, &header[0x118], sizeof(program_id)); + } + + if (iterator->callbacks.encryption.get_3ds_ncch_normal_keys(primary_key_y, secondary_key_x_slot, seed_crypto_flag != 0 ? program_id : NULL, primary_key, secondary_key) == 0) + return rc_hash_iterator_error(iterator, "Could not obtain NCCH normal keys"); + } + + switch (ncch_version) { + case 0: + case 2: + rc_hash_iterator_verbose(iterator, "Detected NCCH version 0/2"); + for (i = 0; i < 8; i++) { + /* First 8 bytes is the partition id in reverse byte order */ + iv[7 - i] = header[0x108 + i]; + } + + /* Magic number for ExeFS */ + iv[8] = 2; + + /* Rest of the bytes are 0 */ + memset(&iv[9], 0, sizeof(iv) - 9); + break; + + case 1: + rc_hash_iterator_verbose(iterator, "Detected NCCH version 1"); + for (i = 0; i < 8; i++) { + /* First 8 bytes is the partition id in normal byte order */ + iv[i] = header[0x108 + i]; + } + + /* Next 4 bytes are 0 */ + memset(&iv[8], 0, 4); + + /* Last 4 bytes is the ExeFS byte offset in big endian */ + iv[12] = (exefs_offset >> 24) & 0xFF; + iv[13] = (exefs_offset >> 16) & 0xFF; + iv[14] = (exefs_offset >> 8) & 0xFF; + iv[15] = exefs_offset & 0xFF; + break; + + default: + return rc_hash_iterator_error_formatted(iterator, "Invalid NCCH version %04X", (unsigned)ncch_version); + } + } + + /* ASSERT: file position must be +0x200 from start of NCCH (i.e. end of header) */ + exefs_offset -= 0x200; + + if (cia_aes) { + /* CBC decryption works by setting the IV to the encrypted previous block. + * Normally this means we would need to decrypt the data between the header and the ExeFS so the CIA AES state is correct. + * However, we can abuse how CBC decryption works and just set the IV to last block we would otherwise decrypt. + * We don't care about the data betweeen the header and ExeFS, so this works fine. */ + + rc_file_seek(iterator, file_handle, (int64_t)exefs_offset - AES_BLOCKLEN, SEEK_CUR); + if (rc_file_read(iterator, file_handle, cia_iv, AES_BLOCKLEN) != AES_BLOCKLEN) + return rc_hash_iterator_error(iterator, "Could not read NCCH data"); + + AES_ctx_set_iv(cia_aes, cia_iv); + } + else { + /* No encryption present, just skip over the in-between data */ + rc_file_seek(iterator, file_handle, (int64_t)exefs_offset, SEEK_CUR); + } + + hash_buffer = (uint8_t*)malloc(exefs_buffer_size); + if (!hash_buffer) + return rc_hash_iterator_error_formatted(iterator, "Failed to allocate %u bytes", (unsigned)exefs_buffer_size); + + /* Clear out crypto flags to ensure we get the same hash for decrypted and encrypted ROMs */ + memset(&header[0x114], 0, 4); + header[0x188 + 3] = 0; + header[0x188 + 7] &= ~(0x20 | 0x04 | 0x01); + + rc_hash_iterator_verbose(iterator, "Hashing 512 byte NCCH header"); + md5_append(md5, header, 0x200); + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u bytes for ExeFS (at NCCH offset %08X%08X)", + (unsigned)exefs_buffer_size, (unsigned)(exefs_offset >> 32), (unsigned)exefs_offset); + + if (rc_file_read(iterator, file_handle, hash_buffer, exefs_buffer_size) != exefs_buffer_size) { + free(hash_buffer); + return rc_hash_iterator_error(iterator, "Could not read ExeFS data"); + } + + if (cia_aes) { + rc_hash_iterator_verbose(iterator, "Performing CIA decryption for ExeFS"); + AES_CBC_decrypt_buffer(cia_aes, hash_buffer, exefs_buffer_size); + } + + if (no_crypto_flag == 0) { + rc_hash_iterator_verbose(iterator, "Performing NCCH decryption for ExeFS"); + + AES_init_ctx_iv(&ncch_aes, primary_key, iv); + AES_CTR_xcrypt_buffer(&ncch_aes, hash_buffer, 0x200); + + for (i = 0; i < 8; i++) { + memcpy(exefs_section_name, &hash_buffer[i * 16], sizeof(exefs_section_name)); + exefs_section_offset = ((uint32_t)hash_buffer[i * 16 + 11] << 24) | (hash_buffer[i * 16 + 10] << 16) | (hash_buffer[i * 16 + 9] << 8) | hash_buffer[i * 16 + 8]; + exefs_section_size = ((uint32_t)hash_buffer[i * 16 + 15] << 24) | (hash_buffer[i * 16 + 14] << 16) | (hash_buffer[i * 16 + 13] << 8) | hash_buffer[i * 16 + 12]; + + /* 0 size indicates an unused section */ + if (exefs_section_size == 0) + continue; + + /* Offsets must be aligned by a media unit */ + if (exefs_section_offset & 0x1FF) + return rc_hash_iterator_error(iterator, "ExeFS section offset is misaligned"); + + /* Offset is relative to the end of the header */ + exefs_section_offset += 0x200; + + /* Check against malformed sections */ + if (exefs_section_offset + ((exefs_section_size + 0x1FF) & ~(uint64_t)0x1FF) > (uint64_t)exefs_real_size) + return rc_hash_iterator_error(iterator, "ExeFS section would overflow"); + + if (memcmp(exefs_section_name, "icon", 4) == 0 || + memcmp(exefs_section_name, "banner", 6) == 0) { + /* Align size up by a media unit */ + exefs_section_size = (exefs_section_size + 0x1FF) & ~(uint64_t)0x1FF; + AES_init_ctx(&ncch_aes, primary_key); + } + else { + /* We don't align size up here, as the padding bytes will use the primary key rather than the secondary key */ + AES_init_ctx(&ncch_aes, secondary_key); + } + + /* In theory, the section offset + size could be greater than the buffer size */ + /* In practice, this likely never occurs, but just in case it does, ignore the section or constrict the size */ + if (exefs_section_offset + exefs_section_size > exefs_buffer_size) { + if (exefs_section_offset >= exefs_buffer_size) + continue; + + exefs_section_size = exefs_buffer_size - exefs_section_offset; + } + + exefs_section_name[7] = '\0'; + rc_hash_iterator_verbose_formatted(iterator, "Decrypting ExeFS file %s at ExeFS offset %08X with size %08X", + (const char*)exefs_section_name, (unsigned)exefs_section_offset, (unsigned)exefs_section_size); + + AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], exefs_section_size & ~(uint64_t)0xF); + + if (exefs_section_size & 0x1FF) { + /* Handle padding bytes, these always use the primary key */ + exefs_section_offset += exefs_section_size; + exefs_section_size = 0x200 - (exefs_section_size & 0x1FF); + + rc_hash_iterator_verbose_formatted(iterator, "Decrypting ExeFS padding at ExeFS offset %08X with size %08X", + (unsigned)exefs_section_offset, (unsigned)exefs_section_size); + + /* Align our decryption start to an AES block boundary */ + if (exefs_section_size & 0xF) { + /* We're a little evil here re-using the IV like this, but this seems to be the best way to deal with this... */ + memcpy(iv, ncch_aes.Iv, sizeof(iv)); + exefs_section_offset &= ~(uint64_t)0xF; + + /* First decrypt these last bytes using the secondary key */ + AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], 0x10 - (exefs_section_size & 0xF)); + + /* Now re-encrypt these bytes using the primary key */ + AES_init_ctx_iv(&ncch_aes, primary_key, iv); + AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], 0x10 - (exefs_section_size & 0xF)); + + /* All of the padding can now be decrypted using the primary key */ + AES_ctx_set_iv(&ncch_aes, iv); + exefs_section_size += 0x10 - (exefs_section_size & 0xF); + } + + AES_init_ctx(&ncch_aes, primary_key); + AES_CTR_xcrypt_buffer(&ncch_aes, &hash_buffer[exefs_section_offset], (size_t)exefs_section_size); + } + } + } + + md5_append(md5, hash_buffer, exefs_buffer_size); + + free(hash_buffer); + return 1; +} + +static uint32_t rc_hash_nintendo_3ds_cia_signature_size(uint8_t header[0x200], const rc_hash_iterator_t* iterator) +{ + uint32_t signature_type; + + signature_type = ((uint32_t)header[0] << 24) | (header[1] << 16) | (header[2] << 8) | header[3]; + switch (signature_type) { + case 0x010000: + case 0x010003: + return 0x200 + 0x3C; + + case 0x010001: + case 0x010004: + return 0x100 + 0x3C; + + case 0x010002: + case 0x010005: + return 0x3C + 0x40; + + default: + return rc_hash_iterator_error_formatted(iterator, "Invalid signature type %08X", (unsigned)signature_type); + } +} + +static int rc_hash_nintendo_3ds_cia(md5_state_t* md5, void* file_handle, uint8_t header[0x200], + const rc_hash_iterator_t* iterator) +{ + const uint32_t CIA_HEADER_SIZE = 0x2020; /* Yes, this is larger than the header[0x200], but we only use the beginning of the header */ + const uint64_t CIA_ALIGNMENT_MASK = 64 - 1; /* sizes are aligned by 64 bytes */ + struct AES_ctx aes; + uint8_t iv[AES_BLOCKLEN], normal_key[AES_KEYLEN], title_key[AES_KEYLEN], title_id[sizeof(uint64_t)]; + uint32_t cert_size, tik_size, tmd_size; + int64_t cert_offset, tik_offset, tmd_offset, content_offset; + uint32_t signature_size, i; + uint16_t content_count; + uint8_t common_key_index; + + cert_size = ((uint32_t)header[0x0B] << 24) | (header[0x0A] << 16) | (header[0x09] << 8) | header[0x08]; + tik_size = ((uint32_t)header[0x0F] << 24) | (header[0x0E] << 16) | (header[0x0D] << 8) | header[0x0C]; + tmd_size = ((uint32_t)header[0x13] << 24) | (header[0x12] << 16) | (header[0x11] << 8) | header[0x10]; + + cert_offset = (CIA_HEADER_SIZE + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; + tik_offset = (cert_offset + cert_size + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; + tmd_offset = (tik_offset + tik_size + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; + content_offset = (tmd_offset + tmd_size + CIA_ALIGNMENT_MASK) & ~CIA_ALIGNMENT_MASK; + + /* Check if this CIA is encrypted, if it isn't, we can hash it right away */ + + rc_file_seek(iterator, file_handle, tmd_offset, SEEK_SET); + if (rc_file_read(iterator, file_handle, header, 4) != 4) + return rc_hash_iterator_error(iterator, "Could not read TMD signature type"); + + signature_size = rc_hash_nintendo_3ds_cia_signature_size(header, iterator); + if (signature_size == 0) + return 0; /* rc_hash_nintendo_3ds_cia_signature_size will call rc_hash_error, so we don't need to do so here */ + + rc_file_seek(iterator, file_handle, signature_size + 0x9E, SEEK_CUR); + if (rc_file_read(iterator, file_handle, header, 2) != 2) + return rc_hash_iterator_error(iterator, "Could not read TMD content count"); + + content_count = (header[0] << 8) | header[1]; + + rc_file_seek(iterator, file_handle, 0x9C4 - 0x9E - 2, SEEK_CUR); + for (i = 0; i < content_count; i++) { + if (rc_file_read(iterator, file_handle, header, 0x30) != 0x30) + return rc_hash_iterator_error(iterator, "Could not read TMD content chunk"); + + /* Content index 0 is the main content (i.e. the 3DS executable) */ + if (((header[4] << 8) | header[5]) == 0) + break; + + content_offset += ((uint32_t)header[0xC] << 24) | (header[0xD] << 16) | (header[0xE] << 8) | header[0xF]; + } + + if (i == content_count) + return rc_hash_iterator_error(iterator, "Could not find main content chunk in TMD"); + + if ((header[7] & 1) == 0) { + /* Not encrypted, we can hash the NCCH immediately */ + rc_file_seek(iterator, file_handle, content_offset, SEEK_SET); + if (rc_file_read(iterator, file_handle, header, 0x200) != 0x200) + return rc_hash_iterator_error(iterator, "Could not read NCCH header"); + + if (memcmp(&header[0x100], "NCCH", 4) != 0) + return rc_hash_iterator_error_formatted(iterator, "NCCH header was not at %08X%08X", (unsigned)(content_offset >> 32), (unsigned)content_offset); + + return rc_hash_nintendo_3ds_ncch(md5, file_handle, header, NULL, iterator); + } + + if (iterator->callbacks.encryption.get_3ds_cia_normal_key == NULL) + return rc_hash_iterator_error(iterator, "An encrypted CIA was detected, but the CIA normal key callback was not set"); + + /* Acquire the encrypted title key, title id, and common key index from the ticket */ + /* These will be needed to decrypt the title key, and that will be needed to decrypt the CIA */ + + rc_file_seek(iterator, file_handle, tik_offset, SEEK_SET); + if (rc_file_read(iterator, file_handle, header, 4) != 4) + return rc_hash_iterator_error(iterator, "Could not read ticket signature type"); + + signature_size = rc_hash_nintendo_3ds_cia_signature_size(header, iterator); + if (signature_size == 0) + return 0; + + rc_file_seek(iterator, file_handle, signature_size, SEEK_CUR); + if (rc_file_read(iterator, file_handle, header, 0xB2) != 0xB2) + return rc_hash_iterator_error(iterator, "Could not read ticket data"); + + memcpy(title_key, &header[0x7F], sizeof(title_key)); + memcpy(title_id, &header[0x9C], sizeof(title_id)); + common_key_index = header[0xB1]; + + if (common_key_index > 5) + return rc_hash_iterator_error_formatted(iterator, "Invalid common key index %02X", (unsigned)common_key_index); + + if (iterator->callbacks.encryption.get_3ds_cia_normal_key(common_key_index, normal_key) == 0) + return rc_hash_iterator_error_formatted(iterator, "Could not obtain common key %02X", (unsigned)common_key_index); + + memset(iv, 0, sizeof(iv)); + memcpy(iv, title_id, sizeof(title_id)); + AES_init_ctx_iv(&aes, normal_key, iv); + + /* Finally, decrypt the title key */ + AES_CBC_decrypt_buffer(&aes, title_key, sizeof(title_key)); + + /* Now we can hash the NCCH */ + + rc_file_seek(iterator, file_handle, content_offset, SEEK_SET); + if (rc_file_read(iterator, file_handle, header, 0x200) != 0x200) + return rc_hash_iterator_error(iterator, "Could not read NCCH header"); + + memset(iv, 0, sizeof(iv)); /* Content index is iv (which is always 0 for main content) */ + AES_init_ctx_iv(&aes, title_key, iv); + AES_CBC_decrypt_buffer(&aes, header, 0x200); + + if (memcmp(&header[0x100], "NCCH", 4) != 0) + return rc_hash_iterator_error_formatted(iterator, "NCCH header was not at %08X%08X", (unsigned)(content_offset >> 32), (unsigned)content_offset); + + return rc_hash_nintendo_3ds_ncch(md5, file_handle, header, &aes, iterator); +} + +static int rc_hash_nintendo_3ds_3dsx(md5_state_t* md5, void* file_handle, uint8_t header[0x200], const rc_hash_iterator_t* iterator) +{ + uint8_t* hash_buffer; + uint32_t header_size, reloc_header_size, code_size; + int64_t code_offset; + + header_size = (header[5] << 8) | header[4]; + reloc_header_size = (header[7] << 8) | header[6]; + code_size = ((uint32_t)header[0x13] << 24) | (header[0x12] << 16) | (header[0x11] << 8) | header[0x10]; + + /* 3 relocation headers are in-between the 3DSX header and code segment */ + code_offset = header_size + reloc_header_size * 3; + + if (code_size > MAX_BUFFER_SIZE) + code_size = MAX_BUFFER_SIZE; + + hash_buffer = (uint8_t*)malloc(code_size); + if (!hash_buffer) + return rc_hash_iterator_error_formatted(iterator, "Failed to allocate %u bytes", (unsigned)code_size); + + rc_file_seek(iterator, file_handle, code_offset, SEEK_SET); + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u bytes for 3DSX (at %08X)", (unsigned)code_size, (unsigned)code_offset); + + if (rc_file_read(iterator, file_handle, hash_buffer, code_size) != code_size) { + free(hash_buffer); + return rc_hash_iterator_error(iterator, "Could not read 3DSX code segment"); + } + + md5_append(md5, hash_buffer, code_size); + + free(hash_buffer); + return 1; +} + +int rc_hash_nintendo_3ds(char hash[33], const rc_hash_iterator_t* iterator) +{ + md5_state_t md5; + void* file_handle; + uint8_t header[0x200]; /* NCCH and NCSD headers are both 0x200 bytes */ + int64_t header_offset; + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + + /* If we don't have a full header, this is probably not a 3DS ROM */ + if (rc_file_read(iterator, file_handle, header, sizeof(header)) != sizeof(header)) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Could not read 3DS ROM header"); + } + + md5_init(&md5); + + if (memcmp(&header[0x100], "NCSD", 4) == 0) { + /* A NCSD container contains 1-8 NCCH partitions */ + /* The first partition (index 0) is reserved for executable content */ + header_offset = ((uint32_t)header[0x123] << 24) | (header[0x122] << 16) | (header[0x121] << 8) | header[0x120]; + /* Offset is in "media units" (1 media unit = 0x200 bytes) */ + header_offset *= 0x200; + + /* We include the NCSD header in the hash, as that will ensure different versions of a game result in a different hash + * This is due to some revisions / languages only ever changing other NCCH paritions (e.g. the game manual) + */ + rc_hash_iterator_verbose(iterator, "Hashing 512 byte NCSD header"); + md5_append(&md5, header, sizeof(header)); + + rc_hash_iterator_verbose_formatted(iterator, + "Detected NCSD header, seeking to NCCH partition at %08X%08X", + (unsigned)(header_offset >> 32), (unsigned)header_offset); + + rc_file_seek(iterator, file_handle, header_offset, SEEK_SET); + if (rc_file_read(iterator, file_handle, header, sizeof(header)) != sizeof(header)) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Could not read 3DS NCCH header"); + } + + if (memcmp(&header[0x100], "NCCH", 4) != 0) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error_formatted(iterator, "3DS NCCH header was not at %08X%08X", (unsigned)(header_offset >> 32), (unsigned)header_offset); + } + } + + if (memcmp(&header[0x100], "NCCH", 4) == 0) { + if (rc_hash_nintendo_3ds_ncch(&md5, file_handle, header, NULL, iterator)) { + rc_file_close(iterator, file_handle); + return rc_hash_finalize(iterator, &md5, hash); + } + + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Failed to hash 3DS NCCH container"); + } + + /* Couldn't identify either an NCSD or NCCH */ + + /* Try to identify this as a CIA */ + if (header[0] == 0x20 && header[1] == 0x20 && header[2] == 0x00 && header[3] == 0x00) { + rc_hash_iterator_verbose(iterator, "Detected CIA, attempting to find executable NCCH"); + + if (rc_hash_nintendo_3ds_cia(&md5, file_handle, header, iterator)) { + rc_file_close(iterator, file_handle); + return rc_hash_finalize(iterator, &md5, hash); + } + + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Failed to hash 3DS CIA container"); + } + + /* This might be a homebrew game, try to detect that */ + if (memcmp(&header[0], "3DSX", 4) == 0) { + rc_hash_iterator_verbose(iterator, "Detected 3DSX"); + + if (rc_hash_nintendo_3ds_3dsx(&md5, file_handle, header, iterator)) { + rc_file_close(iterator, file_handle); + return rc_hash_finalize(iterator, &md5, hash); + } + + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Failed to hash 3DS 3DSX container"); + } + + /* Raw ELF marker (AXF/ELF files) */ + if (memcmp(&header[0], "\x7f\x45\x4c\x46", 4) == 0) { + rc_hash_iterator_verbose(iterator, "Detected AXF/ELF file, hashing entire file"); + + /* Don't bother doing anything fancy here, just hash entire file */ + rc_file_close(iterator, file_handle); + return rc_hash_whole_file(hash, iterator); + } + + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Not a 3DS ROM"); +} diff --git a/deps/rcheevos/src/rhash/hash_rom.c b/deps/rcheevos/src/rhash/hash_rom.c new file mode 100644 index 0000000000..996c3080c3 --- /dev/null +++ b/deps/rcheevos/src/rhash/hash_rom.c @@ -0,0 +1,426 @@ +#include "rc_hash_internal.h" + +#include "../rc_compat.h" + +#include + +/* ===================================================== */ + +static int rc_hash_unheadered_iterator_buffer(char hash[33], const rc_hash_iterator_t* iterator, size_t header_size) +{ + return rc_hash_buffer(hash, iterator->buffer + header_size, iterator->buffer_size - header_size, iterator); +} + +static int rc_hash_iterator_buffer(char hash[33], const rc_hash_iterator_t* iterator) +{ + return rc_hash_buffer(hash, iterator->buffer, iterator->buffer_size, iterator); +} + +/* ===================================================== */ + +int rc_hash_7800(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* if the file contains a header, ignore it */ + if (memcmp(&iterator->buffer[1], "ATARI7800", 9) == 0) { + rc_hash_iterator_verbose(iterator, "Ignoring 7800 header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 128); + } + + return rc_hash_iterator_buffer(hash, iterator); +} + +int rc_hash_arcade(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* arcade hash is just the hash of the filename (no extension) - the cores are pretty stringent about having the right ROM data */ + const char* filename = rc_path_get_filename(iterator->path); + const char* ext = rc_path_get_extension(filename); + char buffer[128]; /* realistically, this should never need more than ~32 characters */ + size_t filename_length = ext - filename - 1; + + /* fbneo supports loading subsystems by using specific folder names. + * if one is found, include it in the hash. + * https://github.com/libretro/FBNeo/blob/master/src/burner/libretro/README.md#emulating-consoles-and-computers + */ + if (filename > iterator->path + 1) { + int include_folder = 0; + const char* folder = filename - 1; + size_t parent_folder_length = 0; + + do { + if (folder[-1] == '/' || folder[-1] == '\\') + break; + + --folder; + } while (folder > iterator->path); + + parent_folder_length = filename - folder - 1; + if (parent_folder_length < 16) { + char* ptr = buffer; + while (folder < filename - 1) + *ptr++ = tolower(*folder++); + *ptr = '\0'; + + folder = buffer; + } + + switch (parent_folder_length) { + case 3: + if (memcmp(folder, "nes", 3) == 0 || /* NES */ + memcmp(folder, "fds", 3) == 0 || /* FDS */ + memcmp(folder, "sms", 3) == 0 || /* Master System */ + memcmp(folder, "msx", 3) == 0 || /* MSX */ + memcmp(folder, "ngp", 3) == 0 || /* NeoGeo Pocket */ + memcmp(folder, "pce", 3) == 0 || /* PCEngine */ + memcmp(folder, "chf", 3) == 0 || /* ChannelF */ + memcmp(folder, "sgx", 3) == 0) /* SuperGrafX */ + include_folder = 1; + break; + case 4: + if (memcmp(folder, "tg16", 4) == 0 || /* TurboGrafx-16 */ + memcmp(folder, "msx1", 4) == 0) /* MSX */ + include_folder = 1; + break; + case 5: + if (memcmp(folder, "neocd", 5) == 0) /* NeoGeo CD */ + include_folder = 1; + break; + case 6: + if (memcmp(folder, "coleco", 6) == 0 || /* Colecovision */ + memcmp(folder, "sg1000", 6) == 0) /* SG-1000 */ + include_folder = 1; + break; + case 7: + if (memcmp(folder, "genesis", 7) == 0) /* Megadrive (Genesis) */ + include_folder = 1; + break; + case 8: + if (memcmp(folder, "gamegear", 8) == 0 || /* Game Gear */ + memcmp(folder, "megadriv", 8) == 0 || /* Megadrive */ + memcmp(folder, "pcengine", 8) == 0 || /* PCEngine */ + memcmp(folder, "channelf", 8) == 0 || /* ChannelF */ + memcmp(folder, "spectrum", 8) == 0) /* ZX Spectrum */ + include_folder = 1; + break; + case 9: + if (memcmp(folder, "megadrive", 9) == 0) /* Megadrive */ + include_folder = 1; + break; + case 10: + if (memcmp(folder, "supergrafx", 10) == 0 || /* SuperGrafX */ + memcmp(folder, "zxspectrum", 10) == 0) /* ZX Spectrum */ + include_folder = 1; + break; + case 12: + if (memcmp(folder, "mastersystem", 12) == 0 || /* Master System */ + memcmp(folder, "colecovision", 12) == 0) /* Colecovision */ + include_folder = 1; + break; + default: + break; + } + + if (include_folder) { + if (parent_folder_length + filename_length + 1 < sizeof(buffer)) { + buffer[parent_folder_length] = '_'; + memcpy(&buffer[parent_folder_length + 1], filename, filename_length); + return rc_hash_buffer(hash, (uint8_t*)&buffer[0], parent_folder_length + filename_length + 1, iterator); + } + } + } + + return rc_hash_buffer(hash, (uint8_t*)filename, filename_length, iterator); +} + +static int rc_hash_text(char hash[33], const rc_hash_iterator_t* iterator) +{ + md5_state_t md5; + const uint8_t* scan = iterator->buffer; + const uint8_t* stop = iterator->buffer + iterator->buffer_size; + + md5_init(&md5); + + do { + const uint8_t* line = scan; + + /* find end of line */ + while (scan < stop && *scan != '\r' && *scan != '\n') + ++scan; + + md5_append(&md5, line, (int)(scan - line)); + + /* include a normalized line ending */ + /* NOTE: this causes a line ending to be hashed at the end of the file, even if one was not present */ + md5_append(&md5, (const uint8_t*)"\n", 1); + + /* skip newline */ + if (scan < stop && *scan == '\r') + ++scan; + if (scan < stop && *scan == '\n') + ++scan; + + } while (scan < stop); + + return rc_hash_finalize(iterator, &md5, hash); +} + +int rc_hash_arduboy(char hash[33], const rc_hash_iterator_t* iterator) +{ + if (iterator->path && rc_path_compare_extension(iterator->path, "arduboy")) { +#ifndef RC_HASH_NO_ZIP + return rc_hash_arduboyfx(hash, iterator); +#else + rc_hash_iterator_verbose(iterator, ".arduboy file processing not enabled"); + return 0; +#endif + } + + if (!iterator->buffer) + return rc_hash_buffered_file(hash, RC_CONSOLE_ARDUBOY, iterator); + + /* https://en.wikipedia.org/wiki/Intel_HEX */ + return rc_hash_text(hash, iterator); +} + +int rc_hash_lynx(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* if the file contains a header, ignore it */ + if (memcmp(&iterator->buffer[0], "LYNX", 5) == 0) { + rc_hash_iterator_verbose(iterator, "Ignoring LYNX header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 64); + } + + return rc_hash_iterator_buffer(hash, iterator); +} + +int rc_hash_nes(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* if the file contains a header, ignore it */ + if (memcmp(&iterator->buffer[0], "NES\x1a", 4) == 0) { + rc_hash_iterator_verbose(iterator, "Ignoring NES header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 16); + } + + if (memcmp(&iterator->buffer[0], "FDS\x1a", 4) == 0) { + rc_hash_iterator_verbose(iterator, "Ignoring FDS header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 16); + } + + return rc_hash_iterator_buffer(hash, iterator); +} + +int rc_hash_n64(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t* buffer; + uint8_t* stop; + const size_t buffer_size = 65536; + md5_state_t md5; + size_t remaining; + void* file_handle; + int is_v64 = 0; + int is_n64 = 0; + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + buffer = (uint8_t*)malloc(buffer_size); + if (!buffer) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error(iterator, "Could not allocate temporary buffer"); + } + stop = buffer + buffer_size; + + /* read first byte so we can detect endianness */ + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + rc_file_read(iterator, file_handle, buffer, 1); + + if (buffer[0] == 0x80) { /* z64 format (big endian [native]) */ + } + else if (buffer[0] == 0x37) { /* v64 format (byteswapped) */ + rc_hash_iterator_verbose(iterator, "converting v64 to z64"); + is_v64 = 1; + } + else if (buffer[0] == 0x40) { /* n64 format (little endian) */ + rc_hash_iterator_verbose(iterator, "converting n64 to z64"); + is_n64 = 1; + } + else if (buffer[0] == 0xE8 || buffer[0] == 0x22) { /* ndd format (don't byteswap) */ + } + else { + free(buffer); + rc_file_close(iterator, file_handle); + + rc_hash_iterator_verbose(iterator, "Not a Nintendo 64 ROM"); + return 0; + } + + /* calculate total file size */ + rc_file_seek(iterator, file_handle, 0, SEEK_END); + remaining = (size_t)rc_file_tell(iterator, file_handle); + if (remaining > MAX_BUFFER_SIZE) + remaining = MAX_BUFFER_SIZE; + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u bytes", (unsigned)remaining); + + /* begin hashing */ + md5_init(&md5); + + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + while (remaining >= buffer_size) { + rc_file_read(iterator, file_handle, buffer, (int)buffer_size); + + if (is_v64) + rc_hash_byteswap16(buffer, stop); + else if (is_n64) + rc_hash_byteswap32(buffer, stop); + + md5_append(&md5, buffer, (int)buffer_size); + remaining -= buffer_size; + } + + if (remaining > 0) { + rc_file_read(iterator, file_handle, buffer, (int)remaining); + + stop = buffer + remaining; + if (is_v64) + rc_hash_byteswap16(buffer, stop); + else if (is_n64) + rc_hash_byteswap32(buffer, stop); + + md5_append(&md5, buffer, (int)remaining); + } + + /* cleanup */ + rc_file_close(iterator, file_handle); + free(buffer); + + return rc_hash_finalize(iterator, &md5, hash); +} + +int rc_hash_nintendo_ds(char hash[33], const rc_hash_iterator_t* iterator) +{ + uint8_t header[512]; + uint8_t* hash_buffer; + uint32_t hash_size, arm9_size, arm9_addr, arm7_size, arm7_addr, icon_addr; + size_t num_read; + int64_t offset = 0; + md5_state_t md5; + void* file_handle; + + file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + rc_file_seek(iterator, file_handle, 0, SEEK_SET); + if (rc_file_read(iterator, file_handle, header, sizeof(header)) != 512) + return rc_hash_iterator_error(iterator, "Failed to read header"); + + if (header[0] == 0x2E && header[1] == 0x00 && header[2] == 0x00 && header[3] == 0xEA && + header[0xB0] == 0x44 && header[0xB1] == 0x46 && header[0xB2] == 0x96 && header[0xB3] == 0) { + /* SuperCard header detected, ignore it */ + rc_hash_iterator_verbose(iterator, "Ignoring SuperCard header"); + + offset = 512; + rc_file_seek(iterator, file_handle, offset, SEEK_SET); + rc_file_read(iterator, file_handle, header, sizeof(header)); + } + + arm9_addr = header[0x20] | (header[0x21] << 8) | (header[0x22] << 16) | (header[0x23] << 24); + arm9_size = header[0x2C] | (header[0x2D] << 8) | (header[0x2E] << 16) | (header[0x2F] << 24); + arm7_addr = header[0x30] | (header[0x31] << 8) | (header[0x32] << 16) | (header[0x33] << 24); + arm7_size = header[0x3C] | (header[0x3D] << 8) | (header[0x3E] << 16) | (header[0x3F] << 24); + icon_addr = header[0x68] | (header[0x69] << 8) | (header[0x6A] << 16) | (header[0x6B] << 24); + + if (arm9_size + arm7_size > 16 * 1024 * 1024) { + /* sanity check - code blocks are typically less than 1MB each - assume not a DS ROM */ + return rc_hash_iterator_error_formatted(iterator, "arm9 code size (%u) + arm7 code size (%u) exceeds 16MB", arm9_size, arm7_size); + } + + hash_size = 0xA00; + if (arm9_size > hash_size) + hash_size = arm9_size; + if (arm7_size > hash_size) + hash_size = arm7_size; + + hash_buffer = (uint8_t*)malloc(hash_size); + if (!hash_buffer) { + rc_file_close(iterator, file_handle); + return rc_hash_iterator_error_formatted(iterator, "Failed to allocate %u bytes", hash_size); + } + + md5_init(&md5); + + rc_hash_iterator_verbose(iterator, "Hashing 352 byte header"); + md5_append(&md5, header, 0x160); + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte arm9 code (at %08X)", arm9_size, arm9_addr); + + rc_file_seek(iterator, file_handle, arm9_addr + offset, SEEK_SET); + rc_file_read(iterator, file_handle, hash_buffer, arm9_size); + md5_append(&md5, hash_buffer, arm9_size); + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u byte arm7 code (at %08X)", arm7_size, arm7_addr); + + rc_file_seek(iterator, file_handle, arm7_addr + offset, SEEK_SET); + rc_file_read(iterator, file_handle, hash_buffer, arm7_size); + md5_append(&md5, hash_buffer, arm7_size); + + rc_hash_iterator_verbose_formatted(iterator, "Hashing 2560 byte icon and labels data (at %08X)", icon_addr); + + rc_file_seek(iterator, file_handle, icon_addr + offset, SEEK_SET); + num_read = rc_file_read(iterator, file_handle, hash_buffer, 0xA00); + if (num_read < 0xA00) { + /* some homebrew games don't provide a full icon block, and no data after the icon block. + * if we didn't get a full icon block, fill the remaining portion with 0s + */ + rc_hash_iterator_verbose_formatted(iterator, + "Warning: only got %u bytes for icon and labels data, 0-padding to 2560 bytes", (unsigned)num_read); + + memset(&hash_buffer[num_read], 0, 0xA00 - num_read); + } + md5_append(&md5, hash_buffer, 0xA00); + + free(hash_buffer); + rc_file_close(iterator, file_handle); + + return rc_hash_finalize(iterator, &md5, hash); +} + +int rc_hash_pce(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* The PCE header doesn't bear any distinguishable marks, so we have to detect + * it by looking at the file size. The core looks for anything that's 512 bytes + * more than a multiple of 8KB, so we'll do that too. + * https://github.com/libretro/beetle-pce-libretro/blob/af28fb0385d00e0292c4703b3aa7e72762b564d2/mednafen/pce/huc.cpp#L196-L202 + */ + if (iterator->buffer_size & 512) { + rc_hash_iterator_verbose(iterator, "Ignoring PCE header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 512); + } + + return rc_hash_iterator_buffer(hash, iterator); +} + +int rc_hash_scv(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* if the file contains a header, ignore it */ + /* https://gitlab.com/MaaaX-EmuSCV/libretro-emuscv/-/blob/master/readme.txt#L211 */ + if (memcmp(iterator->buffer, "EmuSCV", 6) == 0) { + rc_hash_iterator_verbose(iterator, "Ignoring SCV header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 32); + } + + return rc_hash_iterator_buffer(hash, iterator); +} + +int rc_hash_snes(char hash[33], const rc_hash_iterator_t* iterator) +{ + /* if the file contains a header, ignore it */ + uint32_t calc_size = ((uint32_t)iterator->buffer_size / 0x2000) * 0x2000; + if (iterator->buffer_size - calc_size == 512) { + rc_hash_iterator_verbose(iterator, "Ignoring SNES header"); + return rc_hash_unheadered_iterator_buffer(hash, iterator, 512); + } + + return rc_hash_iterator_buffer(hash, iterator); +} diff --git a/deps/rcheevos/src/rhash/hash_zip.c b/deps/rcheevos/src/rhash/hash_zip.c new file mode 100644 index 0000000000..060dc938dd --- /dev/null +++ b/deps/rcheevos/src/rhash/hash_zip.c @@ -0,0 +1,460 @@ +#include "rc_hash_internal.h" + +#include "../rc_compat.h" + +struct rc_hash_zip_idx +{ + size_t length; + uint8_t* data; +}; + +static int rc_hash_zip_idx_sort(const void* a, const void* b) +{ + struct rc_hash_zip_idx* A = (struct rc_hash_zip_idx*)a, * B = (struct rc_hash_zip_idx*)b; + size_t len = (A->length < B->length ? A->length : B->length); + return memcmp(A->data, B->data, len); +} + +typedef int (RC_CCONV* rc_hash_zip_filter_t)(const char* filename, uint32_t filename_len, uint64_t decomp_size, void* userdata); + +static int rc_hash_zip_file(md5_state_t* md5, void* file_handle, + const rc_hash_iterator_t* iterator, + rc_hash_zip_filter_t filter_func, void* filter_userdata) +{ + uint8_t buf[2048], *alloc_buf, *cdir_start, *cdir_max, *cdir, *hashdata, eocdirhdr_size, cdirhdr_size, nparents; + uint32_t cdir_entry_len; + size_t sizeof_idx, indices_offset, alloc_size; + int64_t i_file, archive_size, ecdh_ofs, total_files, cdir_size, cdir_ofs; + struct rc_hash_zip_idx* hashindices, *hashindex; + + rc_file_seek(iterator, file_handle, 0, SEEK_END); + archive_size = rc_file_tell(iterator, file_handle); + + /* Basic sanity checks - reject files which are too small */ + eocdirhdr_size = 22; /* the 'end of central directory header' is 22 bytes */ + if (archive_size < eocdirhdr_size) + return rc_hash_iterator_error(iterator, "ZIP is too small"); + + /* Macros used for reading ZIP and writing to a buffer for hashing (undefined again at the end of the function) */ + #define RC_ZIP_READ_LE16(p) ((uint16_t)(((const uint8_t*)(p))[0]) | ((uint16_t)(((const uint8_t*)(p))[1]) << 8U)) + #define RC_ZIP_READ_LE32(p) ((uint32_t)(((const uint8_t*)(p))[0]) | ((uint32_t)(((const uint8_t*)(p))[1]) << 8U) | ((uint32_t)(((const uint8_t*)(p))[2]) << 16U) | ((uint32_t)(((const uint8_t*)(p))[3]) << 24U)) + #define RC_ZIP_READ_LE64(p) ((uint64_t)(((const uint8_t*)(p))[0]) | ((uint64_t)(((const uint8_t*)(p))[1]) << 8U) | ((uint64_t)(((const uint8_t*)(p))[2]) << 16U) | ((uint64_t)(((const uint8_t*)(p))[3]) << 24U) | ((uint64_t)(((const uint8_t*)(p))[4]) << 32U) | ((uint64_t)(((const uint8_t*)(p))[5]) << 40U) | ((uint64_t)(((const uint8_t*)(p))[6]) << 48U) | ((uint64_t)(((const uint8_t*)(p))[7]) << 56U)) + #define RC_ZIP_WRITE_LE32(p,v) { ((uint8_t*)(p))[0] = (uint8_t)((uint32_t)(v) & 0xFF); ((uint8_t*)(p))[1] = (uint8_t)(((uint32_t)(v) >> 8) & 0xFF); ((uint8_t*)(p))[2] = (uint8_t)(((uint32_t)(v) >> 16) & 0xFF); ((uint8_t*)(p))[3] = (uint8_t)((uint32_t)(v) >> 24); } + #define RC_ZIP_WRITE_LE64(p,v) { ((uint8_t*)(p))[0] = (uint8_t)((uint64_t)(v) & 0xFF); ((uint8_t*)(p))[1] = (uint8_t)(((uint64_t)(v) >> 8) & 0xFF); ((uint8_t*)(p))[2] = (uint8_t)(((uint64_t)(v) >> 16) & 0xFF); ((uint8_t*)(p))[3] = (uint8_t)(((uint64_t)(v) >> 24) & 0xFF); ((uint8_t*)(p))[4] = (uint8_t)(((uint64_t)(v) >> 32) & 0xFF); ((uint8_t*)(p))[5] = (uint8_t)(((uint64_t)(v) >> 40) & 0xFF); ((uint8_t*)(p))[6] = (uint8_t)(((uint64_t)(v) >> 48) & 0xFF); ((uint8_t*)(p))[7] = (uint8_t)((uint64_t)(v) >> 56); } + + /* Find the end of central directory record by scanning the file from the end towards the beginning */ + for (ecdh_ofs = archive_size - sizeof(buf); ; ecdh_ofs -= (sizeof(buf) - 3)) { + int i, n = sizeof(buf); + if (ecdh_ofs < 0) + ecdh_ofs = 0; + if (n > archive_size) + n = (int)archive_size; + + rc_file_seek(iterator, file_handle, ecdh_ofs, SEEK_SET); + if (rc_file_read(iterator, file_handle, buf, n) != (size_t)n) + return rc_hash_iterator_error(iterator, "ZIP read error"); + + for (i = n - 4; i >= 0; --i) { + if (buf[i] == 'P' && RC_ZIP_READ_LE32(buf + i) == 0x06054b50) /* end of central directory header signature */ + break; + } + + if (i >= 0) { + ecdh_ofs += i; + break; + } + + if (!ecdh_ofs || (archive_size - ecdh_ofs) >= (0xFFFF + eocdirhdr_size)) + return rc_hash_iterator_error(iterator, "Failed to find ZIP central directory"); + } + + /* Read and verify the end of central directory record. */ + rc_file_seek(iterator, file_handle, ecdh_ofs, SEEK_SET); + if (rc_file_read(iterator, file_handle, buf, eocdirhdr_size) != eocdirhdr_size) + return rc_hash_iterator_error(iterator, "Failed to read ZIP central directory"); + + /* Read central dir information from end of central directory header */ + total_files = RC_ZIP_READ_LE16(buf + 0x0A); + cdir_size = RC_ZIP_READ_LE32(buf + 0x0C); + cdir_ofs = RC_ZIP_READ_LE32(buf + 0x10); + + /* Check if this is a Zip64 file. In the block of code below: + * - 20 is the size of the ZIP64 end of central directory locator + * - 56 is the size of the ZIP64 end of central directory header + */ + if ((cdir_ofs == 0xFFFFFFFF || cdir_size == 0xFFFFFFFF || total_files == 0xFFFF) && ecdh_ofs >= (20 + 56)) { + /* Read the ZIP64 end of central directory locator if it actually exists */ + rc_file_seek(iterator, file_handle, ecdh_ofs - 20, SEEK_SET); + if (rc_file_read(iterator, file_handle, buf, 20) == 20 && RC_ZIP_READ_LE32(buf) == 0x07064b50) { /* locator signature */ + /* Found the locator, now read the actual ZIP64 end of central directory header */ + int64_t ecdh64_ofs = (int64_t)RC_ZIP_READ_LE64(buf + 0x08); + if (ecdh64_ofs <= (archive_size - 56)) { + rc_file_seek(iterator, file_handle, ecdh64_ofs, SEEK_SET); + if (rc_file_read(iterator, file_handle, buf, 56) == 56 && RC_ZIP_READ_LE32(buf) == 0x06064b50) { /* header signature */ + total_files = RC_ZIP_READ_LE64(buf + 0x20); + cdir_size = RC_ZIP_READ_LE64(buf + 0x28); + cdir_ofs = RC_ZIP_READ_LE64(buf + 0x30); + } + } + } + } + + /* Basic verificaton of central directory (limit to a 256MB content directory) */ + cdirhdr_size = 46; /* the 'central directory header' is 46 bytes */ + if ((cdir_size >= 0x10000000) || (cdir_size < total_files * cdirhdr_size) || ((cdir_ofs + cdir_size) > archive_size)) + return rc_hash_iterator_error(iterator, "Central directory of ZIP file is invalid"); + + /* Allocate once for both directory and our temporary sort index (memory aligned to sizeof(rc_hash_zip_idx)) */ + sizeof_idx = sizeof(struct rc_hash_zip_idx); + indices_offset = (size_t)((cdir_size + sizeof_idx - 1) / sizeof_idx * sizeof_idx); + alloc_size = (size_t)(indices_offset + total_files * sizeof_idx); + alloc_buf = (uint8_t*)malloc(alloc_size); + + /* Read entire central directory to a buffer */ + if (!alloc_buf) + return rc_hash_iterator_error(iterator, "Could not allocate temporary buffer"); + + rc_file_seek(iterator, file_handle, cdir_ofs, SEEK_SET); + if ((int64_t)rc_file_read(iterator, file_handle, alloc_buf, (int)cdir_size) != cdir_size) { + free(alloc_buf); + return rc_hash_iterator_error(iterator, "Failed to read central directory of ZIP file"); + } + + cdir_start = alloc_buf; + cdir_max = cdir_start + cdir_size - cdirhdr_size; + cdir = cdir_start; + + /* Write our temporary hash data to the same buffer we read the central directory from. + * We can do that because the amount of data we keep for each file is guaranteed to be less than the file record. + */ + hashdata = alloc_buf; + hashindices = (struct rc_hash_zip_idx*)(alloc_buf + indices_offset); + hashindex = hashindices; + + /* Now process the central directory file records */ + for (i_file = nparents = 0, cdir = cdir_start; i_file < total_files && cdir >= cdir_start && cdir <= cdir_max; i_file++, cdir += cdir_entry_len) { + const uint8_t* name, * name_end; + uint32_t signature = RC_ZIP_READ_LE32(cdir + 0x00); + uint32_t method = RC_ZIP_READ_LE16(cdir + 0x0A); + uint32_t crc32 = RC_ZIP_READ_LE32(cdir + 0x10); + uint64_t comp_size = RC_ZIP_READ_LE32(cdir + 0x14); + uint64_t decomp_size = RC_ZIP_READ_LE32(cdir + 0x18); + uint32_t filename_len = RC_ZIP_READ_LE16(cdir + 0x1C); + int32_t extra_len = RC_ZIP_READ_LE16(cdir + 0x1E); + int32_t comment_len = RC_ZIP_READ_LE16(cdir + 0x20); + int32_t external_attr = RC_ZIP_READ_LE16(cdir + 0x26); + uint64_t local_hdr_ofs = RC_ZIP_READ_LE32(cdir + 0x2A); + cdir_entry_len = cdirhdr_size + filename_len + extra_len + comment_len; + + if (signature != 0x02014b50) /* expected central directory entry signature */ + break; + + /* Ignore records describing a directory (we only hash file records) */ + name = (cdir + cdirhdr_size); + if (name[filename_len - 1] == '/' || name[filename_len - 1] == '\\' || (external_attr & 0x10)) + continue; + + /* Handle Zip64 fields */ + if (decomp_size == 0xFFFFFFFF || comp_size == 0xFFFFFFFF || local_hdr_ofs == 0xFFFFFFFF) { + int invalid = 0; + const uint8_t* x = cdir + cdirhdr_size + filename_len, * xEnd, * field, * fieldEnd; + for (xEnd = x + extra_len; (x + (sizeof(uint16_t) * 2)) < xEnd; x = fieldEnd) { + field = x + (sizeof(uint16_t) * 2); + fieldEnd = field + RC_ZIP_READ_LE16(x + 2); + if (RC_ZIP_READ_LE16(x) != 0x0001 || fieldEnd > xEnd) + continue; /* Not the Zip64 extended information extra field */ + + if (decomp_size == 0xFFFFFFFF) { + if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { + invalid = 1; + break; + } + + decomp_size = RC_ZIP_READ_LE64(field); + field += sizeof(uint64_t); + } + + if (comp_size == 0xFFFFFFFF) { + if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { + invalid = 1; + break; + } + + comp_size = RC_ZIP_READ_LE64(field); + field += sizeof(uint64_t); + } + + if (local_hdr_ofs == 0xFFFFFFFF) { + if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { + invalid = 1; + break; + } + + local_hdr_ofs = RC_ZIP_READ_LE64(field); + field += sizeof(uint64_t); + } + + break; + } + + if (invalid) { + free(alloc_buf); + return rc_hash_iterator_error(iterator, "Encountered invalid Zip64 file"); + } + } + + /* Basic sanity check on file record */ + /* 30 is the length of the local directory header preceeding the compressed data */ + if ((!method && decomp_size != comp_size) || (decomp_size && !comp_size) || + ((local_hdr_ofs + 30 + comp_size) > (uint64_t)archive_size)) { + free(alloc_buf); + return rc_hash_iterator_error(iterator, "Encountered invalid entry in ZIP central directory"); + } + + if (filter_func) { + int filtered = filter_func((const char*)name, filename_len, decomp_size, filter_userdata); + if (filtered < 0) { + free(alloc_buf); + return 0; + } + + if (filtered) /* this file shouldn't be hashed */ + continue; + } + + /* Write the pointer and length of the data we record about this file */ + hashindex->data = hashdata; + hashindex->length = filename_len + 1 + 4 + 8; + hashindex++; + + rc_hash_iterator_verbose_formatted(iterator, "File in ZIP: %.*s (%u bytes, CRC32 = %08X)", filename_len, (const char*)name, (unsigned)decomp_size, crc32); + + /* Convert and store the file name in the hash data buffer */ + for (name_end = name + filename_len; name != name_end; name++) { + *(hashdata++) = + (*name == '\\' ? '/' : /* convert back-slashes to regular slashes */ + (*name >= 'A' && *name <= 'Z') ? (*name | 0x20) : /* convert upper case letters to lower case */ + *name); /* else use the byte as-is */ + } + + /* Add zero terminator, CRC32 and decompressed size to the hash data buffer */ + *(hashdata++) = '\0'; + RC_ZIP_WRITE_LE32(hashdata, crc32); + hashdata += 4; + RC_ZIP_WRITE_LE64(hashdata, decomp_size); + hashdata += 8; + } + + rc_hash_iterator_verbose_formatted(iterator, "Hashing %u files in ZIP archive", (unsigned)(hashindex - hashindices)); + + /* Sort the file list indices */ + qsort(hashindices, (hashindex - hashindices), sizeof(struct rc_hash_zip_idx), rc_hash_zip_idx_sort); + + /* Hash the data in the order of the now sorted indices */ + for (; hashindices != hashindex; hashindices++) + md5_append(md5, hashindices->data, (int)hashindices->length); + + free(alloc_buf); + + return 1; + + #undef RC_ZIP_READ_LE16 + #undef RC_ZIP_READ_LE32 + #undef RC_ZIP_READ_LE64 + #undef RC_ZIP_WRITE_LE32 + #undef RC_ZIP_WRITE_LE64 +} + +/* ===================================================== */ + +static int rc_hash_arduboyfx_filter(const char* filename, uint32_t filename_len, uint64_t decomp_size, void* userdata) +{ + (void)decomp_size; + (void)userdata; + + /* An .arduboy file is a zip file containing an info.json pointing at one or more bin + * and hex files. It can also contain a bunch of screenshots, but we don't care about + * those. As they're also referenced in the info.json, we have to ignore that too. + * Instead of ignoring the info.json and all image files, only process any bin/hex files */ + if (filename_len > 4) { + const char* ext = &filename[filename_len - 4]; + if (strncasecmp(ext, ".hex", 4) == 0 || strncasecmp(ext, ".bin", 4) == 0) + return 0; /* keep hex and bin */ + } + + return 1; /* filter everything else */ +} + +int rc_hash_arduboyfx(char hash[33], const rc_hash_iterator_t* iterator) +{ + md5_state_t md5; + int res; + + void* file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + md5_init(&md5); + res = rc_hash_zip_file(&md5, file_handle, iterator, rc_hash_arduboyfx_filter, NULL); + rc_file_close(iterator, file_handle); + + if (!res) + return 0; + + return rc_hash_finalize(iterator, &md5, hash); +} + +/* ===================================================== */ + +struct rc_hash_ms_dos_dosz_state { + const char* path; + const struct rc_hash_ms_dos_dosz_state* child; + + md5_state_t* md5; + const rc_hash_iterator_t* iterator; + void* file_handle; + uint32_t nparents; +}; + +static int rc_hash_dosz(struct rc_hash_ms_dos_dosz_state* dosz); + +static int rc_hash_ms_dos_parent(const struct rc_hash_ms_dos_dosz_state* child, + const char* parentname, uint32_t parentname_len) +{ + const char* lastfslash = strrchr(child->path, '/'); + const char* lastbslash = strrchr(child->path, '\\'); + const char* lastslash = (lastbslash > lastfslash ? lastbslash : lastfslash); + size_t dir_len = (lastslash ? (lastslash + 1 - child->path) : 0); + char* parent_path = (char*)malloc(dir_len + parentname_len + 1); + struct rc_hash_ms_dos_dosz_state parent; + const struct rc_hash_ms_dos_dosz_state* check; + void* parent_handle; + int parent_res; + + /* Build the path of the parent by combining the directory of the current file with the name */ + if (!parent_path) + return rc_hash_iterator_error(child->iterator, "Could not allocate temporary buffer"); + + memcpy(parent_path, child->path, dir_len); + memcpy(parent_path + dir_len, parentname, parentname_len); + parent_path[dir_len + parentname_len] = '\0'; + + /* Make sure there is no recursion where a parent DOSZ is an already seen child DOSZ */ + for (check = child->child; check; check = check->child) { + if (!strcmp(check->path, parent_path)) { + free(parent_path); + return rc_hash_iterator_error(child->iterator, "Invalid DOSZ file with recursive parents"); + } + } + + /* Try to open the parent DOSZ file */ + parent_handle = rc_file_open(child->iterator, parent_path); + if (!parent_handle) { + rc_hash_iterator_error_formatted(child->iterator, "DOSZ parent file '%s' does not exist", parent_path); + free(parent_path); + return 0; + } + + /* Fully hash the parent DOSZ ahead of the child */ + memcpy(&parent, child, sizeof(parent)); + parent.path = parent_path; + parent.child = child; + parent.file_handle = parent_handle; + parent_res = rc_hash_dosz(&parent); + rc_file_close(child->iterator, parent_handle); + free(parent_path); + return parent_res; +} + +static int rc_hash_ms_dos_dosc(const struct rc_hash_ms_dos_dosz_state* dosz) +{ + size_t path_len = strlen(dosz->path); + if (dosz->path[path_len - 1] == 'z' || dosz->path[path_len - 1] == 'Z') { + void* file_handle; + char* dosc_path = strdup(dosz->path); + if (!dosc_path) + return rc_hash_iterator_error(dosz->iterator, "Could not allocate temporary buffer"); + + /* Swap the z to c and use the same capitalization, hash the file if it exists */ + dosc_path[path_len - 1] = (dosz->path[path_len - 1] == 'z' ? 'c' : 'C'); + file_handle = rc_file_open(dosz->iterator, dosc_path); + free(dosc_path); + + if (file_handle) { + /* Hash the entire contents of the DOSC file */ + int res = rc_hash_zip_file(dosz->md5, file_handle, dosz->iterator, NULL, NULL); + rc_file_close(dosz->iterator, file_handle); + if (!res) + return 0; + } + } + + return 1; +} + +static int rc_hash_dosz_filter(const char* filename, uint32_t filename_len, uint64_t decomp_size, void* userdata) +{ + struct rc_hash_ms_dos_dosz_state* dosz = (struct rc_hash_ms_dos_dosz_state*)userdata; + + /* A DOSZ file can contain a special empty .dosz.parent file in its root which means a parent dosz file is used */ + if (decomp_size == 0 && filename_len > 7 && + strncasecmp(&filename[filename_len - 7], ".parent", 7) == 0 && + !memchr(filename, '/', filename_len) && + !memchr(filename, '\\', filename_len)) + { + /* A DOSZ file can only have one parent file */ + if (dosz->nparents++) + return -1; + + /* process the parent. if it fails, stop */ + if (!rc_hash_ms_dos_parent(dosz, filename, (filename_len - 7))) + return -1; + + /* We don't hash this meta file so a user is free to rename it and the parent file */ + return 1; + } + + return 0; +} + +static int rc_hash_dosz(struct rc_hash_ms_dos_dosz_state* dosz) +{ + if (!rc_hash_zip_file(dosz->md5, dosz->file_handle, dosz->iterator, rc_hash_dosz_filter, dosz)) + return 0; + + /* A DOSZ file can only have one parent file */ + if (dosz->nparents > 1) + return rc_hash_iterator_error(dosz->iterator, "Invalid DOSZ file with multiple parents"); + + /* Check if an associated .dosc file exists */ + if (!rc_hash_ms_dos_dosc(dosz)) + return 0; + + return 1; +} + +int rc_hash_ms_dos(char hash[33], const rc_hash_iterator_t* iterator) +{ + struct rc_hash_ms_dos_dosz_state dosz; + md5_state_t md5; + int res; + + void* file_handle = rc_file_open(iterator, iterator->path); + if (!file_handle) + return rc_hash_iterator_error(iterator, "Could not open file"); + + memset(&dosz, 0, sizeof(dosz)); + dosz.path = iterator->path; + dosz.file_handle = file_handle; + dosz.iterator = iterator; + dosz.md5 = &md5; + + md5_init(&md5); + res = rc_hash_dosz(&dosz); + rc_file_close(iterator, file_handle); + + if (!res) + return 0; + + return rc_hash_finalize(iterator, &md5, hash); +} diff --git a/deps/rcheevos/src/rhash/rc_hash_internal.h b/deps/rcheevos/src/rhash/rc_hash_internal.h new file mode 100644 index 0000000000..f50a9348d2 --- /dev/null +++ b/deps/rcheevos/src/rhash/rc_hash_internal.h @@ -0,0 +1,116 @@ +#ifndef RC_HASH_INTERNAL_H +#define RC_HASH_INTERNAL_H + +#include "rc_hash.h" +#include "md5.h" + +RC_BEGIN_C_DECLS + +/* hash.c */ + +void* rc_file_open(const rc_hash_iterator_t* iterator, const char* path); +void rc_file_seek(const rc_hash_iterator_t* iterator, void* file_handle, int64_t offset, int origin); +int64_t rc_file_tell(const rc_hash_iterator_t* iterator, void* file_handle); +size_t rc_file_read(const rc_hash_iterator_t* iterator, void* file_handle, void* buffer, int requested_bytes); +void rc_file_close(const rc_hash_iterator_t* iterator, void* file_handle); +int64_t rc_file_size(const rc_hash_iterator_t* iterator, const char* path); + + +void rc_hash_iterator_verbose(const rc_hash_iterator_t* iterator, const char* message); +void rc_hash_iterator_verbose_formatted(const rc_hash_iterator_t* iterator, const char* format, ...); +int rc_hash_iterator_error(const rc_hash_iterator_t* iterator, const char* message); +int rc_hash_iterator_error_formatted(const rc_hash_iterator_t* iterator, const char* format, ...); + + +/* arbitrary limit to prevent allocating and hashing large files */ +#define MAX_BUFFER_SIZE 64 * 1024 * 1024 + +void rc_hash_merge_callbacks(rc_hash_iterator_t* iterator, const rc_hash_callbacks_t* callbacks); +int rc_hash_finalize(const rc_hash_iterator_t* iterator, md5_state_t* md5, char hash[33]); + +int rc_hash_buffer(char hash[33], const uint8_t* buffer, size_t buffer_size, const rc_hash_iterator_t* iterator); +void rc_hash_byteswap16(uint8_t* buffer, const uint8_t* stop); +void rc_hash_byteswap32(uint8_t* buffer, const uint8_t* stop); + + +const char* rc_path_get_filename(const char* path); +const char* rc_path_get_extension(const char* path); +int rc_path_compare_extension(const char* path, const char* ext); + + +typedef void (RC_CCONV* rc_hash_iterator_ext_handler_t)(rc_hash_iterator_t* iterator, int data); +typedef struct rc_hash_iterator_ext_handler_entry_t { + char ext[8]; + rc_hash_iterator_ext_handler_t handler; + int data; +} rc_hash_iterator_ext_handler_entry_t; + +const rc_hash_iterator_ext_handler_entry_t* rc_hash_get_iterator_ext_handlers(size_t* num_handlers); + + +typedef struct rc_hash_cdrom_track_t { + void* file_handle; /* the file handle for reading the track data */ + const rc_hash_filereader_t* file_reader; /* functions to perform raw file I/O */ + int64_t file_track_offset;/* the offset of the track data within the file */ + 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 */ + int track_first_sector; /* the first absolute sector associated to the track (includes pregap) */ + int track_pregap_sectors; /* the number of pregap sectors */ +#ifndef NDEBUG + uint32_t track_id; /* the index of the track */ +#endif +} rc_hash_cdrom_track_t; + + +int rc_hash_whole_file(char hash[33], const rc_hash_iterator_t* iterator); +int rc_hash_buffered_file(char hash[33], uint32_t console_id, const rc_hash_iterator_t* iterator); + +#ifndef RC_HASH_NO_ROM + /* hash_rom.c */ + int rc_hash_7800(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_arcade(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_arduboy(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_lynx(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_nes(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_n64(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_nintendo_ds(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_pce(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_scv(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_snes(char hash[33], const rc_hash_iterator_t* iterator); +#endif + +#ifndef RC_HASH_NO_DISC + /* hash_disc.c */ + void rc_hash_reset_iterator_disc(rc_hash_iterator_t* iterator); + + int rc_hash_3do(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_dreamcast(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_gamecube(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_jaguar_cd(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_neogeo_cd(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_pce_cd(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_pcfx_cd(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_psx(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_ps2(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_psp(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_sega_cd(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_wii(char hash[33], const rc_hash_iterator_t* iterator); +#endif + +#ifndef RC_HASH_NO_ENCRYPTED + /* hash_encrypted.c */ + void rc_hash_reset_iterator_encrypted(rc_hash_iterator_t* iterator); + + int rc_hash_nintendo_3ds(char hash[33], const rc_hash_iterator_t* iterator); +#endif + +#ifndef RC_HASH_NO_ZIP + /* hash_zip.c */ + int rc_hash_ms_dos(char hash[33], const rc_hash_iterator_t* iterator); + int rc_hash_arduboyfx(char hash[33], const rc_hash_iterator_t* iterator); +#endif + +RC_END_C_DECLS + +#endif /* RC_HASH_INTERNAL_H */ diff --git a/gfx/widgets/gfx_widget_leaderboard_display.c b/gfx/widgets/gfx_widget_leaderboard_display.c index 756e635f04..7bc58a5512 100644 --- a/gfx/widgets/gfx_widget_leaderboard_display.c +++ b/gfx/widgets/gfx_widget_leaderboard_display.c @@ -50,7 +50,7 @@ struct progress_tracker_info retro_time_t show_until; }; -#define CHEEVO_LBOARD_FIRST_FIXED_CHAR 0x2D /* -./0123456789: */ +#define CHEEVO_LBOARD_FIRST_FIXED_CHAR 0x2C /* ,-./0123456789: */ #define CHEEVO_LBOARD_LAST_FIXED_CHAR 0x3A /* TODO: rename; this file handles all achievement tracker information, not just leaderboards */ diff --git a/griffin/griffin.c b/griffin/griffin.c index e2c6de5080..93529e8bc6 100644 --- a/griffin/griffin.c +++ b/griffin/griffin.c @@ -226,6 +226,10 @@ ACHIEVEMENTS #include "../deps/rcheevos/src/rhash/aes.c" #include "../deps/rcheevos/src/rhash/cdreader.c" #include "../deps/rcheevos/src/rhash/hash.c" +#include "../deps/rcheevos/src/rhash/hash_rom.c" +#include "../deps/rcheevos/src/rhash/hash_disc.c" +#include "../deps/rcheevos/src/rhash/hash_zip.c" +#include "../deps/rcheevos/src/rhash/hash_encrypted.c" #endif diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index 01e075816f..2ac6b605a1 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -10302,6 +10302,14 @@ MSG_HASH( MENU_ENUM_SUBLABEL_ACHIEVEMENT_RESUME_CANCEL, "Leave achievements hardcore mode disabled for the current session" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_RESUME_REQUIRES_RELOAD, + "Resume Achievements Hardcore Mode Disabled" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_ACHIEVEMENT_RESUME_REQUIRES_RELOAD, + "You must reload the core to resume Achievements Hardcore Mode" + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_ACHIEVEMENT_PAUSE, "Pause Achievements Hardcore Mode" diff --git a/menu/cbs/menu_cbs_ok.c b/menu/cbs/menu_cbs_ok.c index ad635e0821..769cb0311b 100644 --- a/menu/cbs/menu_cbs_ok.c +++ b/menu/cbs/menu_cbs_ok.c @@ -9164,6 +9164,7 @@ static int menu_cbs_init_bind_ok_compare_label(menu_file_list_cbs_t *cbs, {MENU_ENUM_LABEL_ACHIEVEMENT_PAUSE_CANCEL, action_ok_close_submenu}, {MENU_ENUM_LABEL_ACHIEVEMENT_RESUME, action_ok_cheevos_toggle_hardcore_mode}, {MENU_ENUM_LABEL_ACHIEVEMENT_RESUME_CANCEL, action_ok_close_submenu }, + {MENU_ENUM_LABEL_ACHIEVEMENT_RESUME_REQUIRES_RELOAD, action_ok_close_submenu }, {MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST, action_ok_push_manual_content_scan_list}, {MENU_ENUM_LABEL_AUDIO_OUTPUT_SETTINGS, action_ok_push_audio_output_settings_list}, #ifdef HAVE_MICROPHONE diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index eb1d413a85..9209764f10 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -309,6 +309,7 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_information_list_list, MENU_ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_achievement_list, MENU_ENUM_SUBLABEL_ACHIEVEMENT_LIST) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_achievement_pause_cancel, MENU_ENUM_SUBLABEL_ACHIEVEMENT_PAUSE_CANCEL) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_achievement_resume_cancel, MENU_ENUM_SUBLABEL_ACHIEVEMENT_RESUME_CANCEL) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_achievement_resume_requires_reload, MENU_ENUM_SUBLABEL_ACHIEVEMENT_RESUME_REQUIRES_RELOAD) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_achievement_server_unreachable,MENU_ENUM_SUBLABEL_ACHIEVEMENT_SERVER_UNREACHABLE) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_cheevos_enable, MENU_ENUM_SUBLABEL_CHEEVOS_ENABLE) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_cheevos_test_unofficial, MENU_ENUM_SUBLABEL_CHEEVOS_TEST_UNOFFICIAL) @@ -4835,6 +4836,9 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_ACHIEVEMENT_RESUME_CANCEL: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_achievement_resume_cancel); break; + case MENU_ENUM_LABEL_ACHIEVEMENT_RESUME_REQUIRES_RELOAD: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_achievement_resume_requires_reload); + break; case MENU_ENUM_LABEL_ACHIEVEMENT_SERVER_UNREACHABLE: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_achievement_server_unreachable); break; diff --git a/msg_hash.h b/msg_hash.h index 6b8b381530..bb36db63fb 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -3092,6 +3092,7 @@ enum msg_hash_enums MENU_LABEL(ACHIEVEMENT_PAUSE_MENU), MENU_LABEL(ACHIEVEMENT_PAUSE_CANCEL), MENU_LABEL(ACHIEVEMENT_RESUME_CANCEL), + MENU_LABEL(ACHIEVEMENT_RESUME_REQUIRES_RELOAD), MENU_LABEL(ACHIEVEMENT_PAUSE), MENU_LABEL(ACHIEVEMENT_RESUME), MENU_LABEL(ACHIEVEMENT_SERVER_UNREACHABLE),