diff --git a/cheevos/cheevos.c b/cheevos/cheevos.c index 0ff30b7bd1..c5f522fede 100644 --- a/cheevos/cheevos.c +++ b/cheevos/cheevos.c @@ -1731,7 +1731,7 @@ void rcheevos_validate_config_settings(void) const settings_t* settings = config_get_ptr(); unsigned console_id; - if (!sysinfo->library_name || !rcheevos_hardcore_active()) + if (!rcheevos_hardcore_active()) return; /* this adds a sleep to every frame. if the value is high enough that a @@ -1793,30 +1793,28 @@ void rcheevos_validate_config_settings(void) return; } - - if (!(disallowed_settings - = rc_libretro_get_disallowed_settings(sysinfo->library_name))) + if (!sysinfo->library_name) return; - if (!retroarch_ctl(RARCH_CTL_CORE_OPTIONS_LIST_GET, &coreopts)) - return; - - for (i = 0; i < (int)coreopts->size; i++) + disallowed_settings = rc_libretro_get_disallowed_settings(sysinfo->library_name); + if (disallowed_settings && retroarch_ctl(RARCH_CTL_CORE_OPTIONS_LIST_GET, &coreopts)) { - const char* key = coreopts->opts[i].key; - const char* val = core_option_manager_get_val(coreopts, i); - if (!rc_libretro_is_setting_allowed(disallowed_settings, key, val)) + for (i = 0; i < (int)coreopts->size; i++) { - char buffer[128]; - /* TODO/FIXME - localize */ - snprintf(buffer, sizeof(buffer), "Hardcore paused. Setting not allowed: %s=%s", key, val); - CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", buffer); - rcheevos_pause_hardcore(); + const char* key = coreopts->opts[i].key; + const char* val = core_option_manager_get_val(coreopts, i); + if (!rc_libretro_is_setting_allowed(disallowed_settings, key, val)) + { + char buffer[128]; + /* TODO/FIXME - localize */ + snprintf(buffer, sizeof(buffer), "Hardcore paused. Setting not allowed: %s=%s", key, val); + CHEEVOS_LOG(RCHEEVOS_TAG "%s\n", buffer); + rcheevos_pause_hardcore(); - runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL, - MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); - - break; + runloop_msg_queue_push(buffer, 0, 4 * 60, false, NULL, + MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); + return; + } } } @@ -2570,6 +2568,7 @@ static void rcheevos_client_load_game_callback(int result, { /* hardcore is active. we're going to start processing * achievements. make sure restrictions are enforced */ + rcheevos_validate_config_settings(); rcheevos_enforce_hardcore_settings(); } else diff --git a/deps/rcheevos/CHANGELOG.md b/deps/rcheevos/CHANGELOG.md index 8f6a1db16c..0d183fbcd8 100644 --- a/deps/rcheevos/CHANGELOG.md +++ b/deps/rcheevos/CHANGELOG.md @@ -1,3 +1,35 @@ +# v11.5.0 +* add total_entries to rc_api_fetch_leaderboard_info_response_t +* add RC_CLIENT_RAINTEGRATION_EVENT_MENU_CHANGED event +* modify rc_client_begin_identify_and_load_game and rc_client_begin_change_media to use locally + registered filereader/cdreader for hash resolution when using rc_client_raintegration +* add support for ISO-8601 timestamps in JSON responses +* update RC_CONSOLE_MS_DOS hash logic to support parent archives +* fix infinite loop that sometimes occurs when resetting while progress tracker is onscreen + +# v11.4.0 +* add RC_CONDITION_REMEMBER and RC_OPERAND_RECALL +* add RC_OPERATOR_ADD and RC_OPERATOR_SUB +* add scratch pad memory to PSX memory map +* add Super Game Module memory to Colecovision memory map +* add rapi function fetch_game_titles +* modify progress functions to return RC_NO_GAME_LOADED when "Unknown Game" is loaded +* update subsystem list for arcade hash +* fix exception if server sends null as achievement.author + +# v11.3.0 +* add RC_OPERATOR_MOD +* add cartridge RAM to Game Gear and Master System memory maps +* add extended cartridge RAM to Gameboy and Gameboy Color memory maps +* add rc_client_is_game_loaded helper function +* add rc_client_raintegration_set_console_id to specify console in case game resolution fails +* add rc_client_raintegration_get_achievement_state to detect local unlocks +* report validation errors on multi-condition logic +* hash whole file for PSP homebrew files (eboot.pbp) +* call DrawMenuBar in rc_client_raintegration_rebuild_submenu if menu changes +* fix file sharing issue using default filereader on Windows +* fix exception calling rc_client_get_game_summary with an unidentified game loaded + # v11.2.0 * add alternate methods for state serialization/deserialization that accept a buffer_size parameter * add RC_CLIENT_SUPPORTS_HASH compile flag diff --git a/deps/rcheevos/include/rc_api_info.h b/deps/rcheevos/include/rc_api_info.h index 918bb6bc04..7d6cfa2bea 100644 --- a/deps/rcheevos/include/rc_api_info.h +++ b/deps/rcheevos/include/rc_api_info.h @@ -129,6 +129,9 @@ typedef struct rc_api_fetch_leaderboard_info_response_t { /* The number of items in the entries array */ uint32_t num_entries; + /* The total number of entries on the server */ + uint32_t total_entries; + /* Common server-provided response information */ rc_api_response_t response; } @@ -180,6 +183,48 @@ RC_EXPORT int RC_CCONV rc_api_process_fetch_games_list_response(rc_api_fetch_gam 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); RC_EXPORT void RC_CCONV rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response); +/* --- Fetch Game Titles --- */ + +/** + * API parameters for a fetch games list request. + */ +typedef struct rc_api_fetch_game_titles_request_t { + /* An array of game ids to fetch titles for */ + const uint32_t* game_ids; + /* The number of items in the game_ids array */ + uint32_t num_game_ids; +} +rc_api_fetch_game_titles_request_t; + +/* A game title entry */ +typedef struct rc_api_game_title_entry_t { + /* The unique identifier of the game */ + uint32_t id; + /* The title of the game */ + const char* title; + /* The image name for the game badge */ + const char* image_name; +} +rc_api_game_title_entry_t; + +/** + * Response data for a fetch games title request. + */ +typedef struct rc_api_fetch_game_titles_response_t { + /* An array of requested entries */ + rc_api_game_title_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_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_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); + RC_END_C_DECLS #endif /* RC_API_INFO_H */ diff --git a/deps/rcheevos/include/rc_client.h b/deps/rcheevos/include/rc_client.h index 1fa50357c5..26db52932d 100644 --- a/deps/rcheevos/include/rc_client.h +++ b/deps/rcheevos/include/rc_client.h @@ -517,6 +517,7 @@ typedef struct rc_client_leaderboard_entry_t { typedef struct rc_client_leaderboard_entry_list_t { rc_client_leaderboard_entry_t* entries; uint32_t num_entries; + uint32_t total_entries; int32_t user_index; } rc_client_leaderboard_entry_list_t; diff --git a/deps/rcheevos/include/rc_client_raintegration.h b/deps/rcheevos/include/rc_client_raintegration.h index 2aa33dfdb4..847c8ad225 100644 --- a/deps/rcheevos/include/rc_client_raintegration.h +++ b/deps/rcheevos/include/rc_client_raintegration.h @@ -39,7 +39,8 @@ enum { RC_CLIENT_RAINTEGRATION_EVENT_TYPE_NONE = 0, RC_CLIENT_RAINTEGRATION_EVENT_MENUITEM_CHECKED_CHANGED = 1, /* [menu_item] checked changed */ RC_CLIENT_RAINTEGRATION_EVENT_HARDCORE_CHANGED = 2, /* hardcore was enabled or disabled */ - RC_CLIENT_RAINTEGRATION_EVENT_PAUSE = 3 /* emulated system should be paused */ + RC_CLIENT_RAINTEGRATION_EVENT_PAUSE = 3, /* emulated system should be paused */ + RC_CLIENT_RAINTEGRATION_EVENT_MENU_CHANGED = 4 /* one or more items were added/removed from the menu and it should be rebuilt */ }; typedef struct rc_client_raintegration_event_t { diff --git a/deps/rcheevos/include/rc_error.h b/deps/rcheevos/include/rc_error.h index 403f55446f..171bbf8e94 100644 --- a/deps/rcheevos/include/rc_error.h +++ b/deps/rcheevos/include/rc_error.h @@ -46,7 +46,9 @@ enum { RC_ACCESS_DENIED = -33, RC_INVALID_CREDENTIALS = -34, RC_EXPIRED_TOKEN = -35, - RC_INSUFFICIENT_BUFFER = -36 + RC_INSUFFICIENT_BUFFER = -36, + RC_INVALID_VARIABLE_NAME = -37, + RC_UNKNOWN_VARIABLE_NAME = -38 }; RC_EXPORT const char* RC_CCONV rc_error_str(int ret); diff --git a/deps/rcheevos/include/rc_runtime_types.h b/deps/rcheevos/include/rc_runtime_types.h index f836dcb688..4bf1b13bfc 100644 --- a/deps/rcheevos/include/rc_runtime_types.h +++ b/deps/rcheevos/include/rc_runtime_types.h @@ -106,7 +106,8 @@ enum { RC_OPERAND_LUA, /* A Lua 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. */ + RC_OPERAND_INVERTED, /* The twos-complement value of a live address in RAM. */ + RC_OPERAND_RECALL /* The value captured by the last RC_CONDITION_REMEMBER condition */ }; typedef struct rc_operand_t { @@ -154,6 +155,7 @@ enum { RC_CONDITION_ADD_SOURCE, /* everything from this point on affects the condition after it */ RC_CONDITION_SUB_SOURCE, RC_CONDITION_ADD_ADDRESS, + RC_CONDITION_REMEMBER, /* logic flags (second switch) */ RC_CONDITION_ADD_HITS, @@ -176,7 +178,9 @@ enum { RC_OPERATOR_DIV, RC_OPERATOR_AND, RC_OPERATOR_XOR, - RC_OPERATOR_MOD + RC_OPERATOR_MOD, + RC_OPERATOR_ADD, + RC_OPERATOR_SUB }; typedef struct rc_condition_t rc_condition_t; @@ -287,6 +291,8 @@ RC_EXPORT void RC_CCONV rc_reset_trigger(rc_trigger_t* self); | Values | \*****************************************************************************/ +#define RC_VALUE_MAX_NAME_LENGTH 15 + struct rc_value_t { /* The current value of the variable. */ rc_memref_value_t value; diff --git a/deps/rcheevos/src/rapi/rc_api_common.c b/deps/rcheevos/src/rapi/rc_api_common.c index ea29c2383c..0b9d1f26b3 100644 --- a/deps/rcheevos/src/rapi/rc_api_common.c +++ b/deps/rcheevos/src/rapi/rc_api_common.c @@ -864,8 +864,11 @@ int rc_json_get_datetime(time_t* out, const rc_json_field_t* field, const char* if (*field->value_start == '\"') { memset(&tm, 0, sizeof(tm)); - if (sscanf_s(field->value_start + 1, "%d-%d-%d %d:%d:%d", - &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) { + if (sscanf_s(field->value_start + 1, "%d-%d-%d %d:%d:%d", /* DB format "2013-10-20 22:12:21" */ + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6 || + /* NOTE: relies on sscanf stopping when it sees a non-digit after the seconds. could be 'Z', '.', '+', or '-' */ + sscanf_s(field->value_start + 1, "%d-%d-%dT%d:%d:%d", /* ISO format "2013-10-20T22:12:21.000000Z */ + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) == 6) { tm.tm_mon--; /* 0-based */ tm.tm_year -= 1900; /* 1900 based */ @@ -938,6 +941,27 @@ int rc_json_get_required_bool(int* out, rc_api_response_t* response, const rc_js return rc_json_missing_field(response, field); } +void rc_json_extract_filename(rc_json_field_t* field) { + if (field->value_end) { + const char* str = field->value_end; + + /* remove the extension */ + while (str > field->value_start && str[-1] != '/') { + --str; + if (*str == '.') { + field->value_end = str; + break; + } + } + + /* find the path separator */ + while (str > field->value_start && str[-1] != '/') + --str; + + field->value_start = str; + } +} + /* --- rc_api_request --- */ void rc_api_destroy_request(rc_api_request_t* request) diff --git a/deps/rcheevos/src/rapi/rc_api_common.h b/deps/rcheevos/src/rapi/rc_api_common.h index 7311cfff55..538fdbba12 100644 --- a/deps/rcheevos/src/rapi/rc_api_common.h +++ b/deps/rcheevos/src/rapi/rc_api_common.h @@ -67,6 +67,8 @@ int rc_json_get_array_entry_object(rc_json_field_t* fields, size_t field_count, int rc_json_get_next_object_field(rc_json_iterator_t* iterator, rc_json_field_t* field); int rc_json_get_object_string_length(const char* json); +void rc_json_extract_filename(rc_json_field_t* field); + void rc_url_builder_append_encoded_str(rc_api_url_builder_t* builder, const char* str); void rc_url_builder_append_num_param(rc_api_url_builder_t* builder, const char* param, int32_t value); void rc_url_builder_append_unum_param(rc_api_url_builder_t* builder, const char* param, uint32_t value); diff --git a/deps/rcheevos/src/rapi/rc_api_info.c b/deps/rcheevos/src/rapi/rc_api_info.c index 2b9f882625..0339f256d4 100644 --- a/deps/rcheevos/src/rapi/rc_api_info.c +++ b/deps/rcheevos/src/rapi/rc_api_info.c @@ -3,6 +3,8 @@ #include "rc_runtime_types.h" +#include "../rc_compat.h" + #include #include @@ -187,7 +189,8 @@ int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboa RC_JSON_NEW_FIELD("LBAuthor"), RC_JSON_NEW_FIELD("LBCreated"), RC_JSON_NEW_FIELD("LBUpdated"), - RC_JSON_NEW_FIELD("Entries") /* array */ + RC_JSON_NEW_FIELD("Entries"), /* array */ + RC_JSON_NEW_FIELD("TotalEntries") /* unused fields RC_JSON_NEW_FIELD("GameTitle"), RC_JSON_NEW_FIELD("ConsoleID"), @@ -233,6 +236,8 @@ int rc_api_process_fetch_leaderboard_info_server_response(rc_api_fetch_leaderboa return RC_MISSING_VALUE; if (!rc_json_get_required_datetime(&response->updated, &response->response, &leaderboarddata_fields[9], "LBUpdated")) return RC_MISSING_VALUE; + if (!rc_json_get_required_unum(&response->total_entries, &response->response, &leaderboarddata_fields[11], "TotalEntries")) + return RC_MISSING_VALUE; if (!leaderboarddata_fields[1].value_end) return RC_MISSING_VALUE; @@ -371,3 +376,91 @@ int rc_api_process_fetch_games_list_server_response(rc_api_fetch_games_list_resp void rc_api_destroy_fetch_games_list_response(rc_api_fetch_games_list_response_t* response) { rc_buffer_destroy(&response->response.buffer); } + +/* --- 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) { + rc_api_url_builder_t builder; + char num[16]; + uint32_t i; + + rc_api_url_build_dorequest_url(request); + + if (api_params->num_game_ids == 0) + return RC_INVALID_STATE; + + rc_url_builder_init(&builder, &request->buffer, 48); + rc_url_builder_append_str_param(&builder, "r", "gameinfolist"); + rc_url_builder_append_unum_param(&builder, "g", api_params->game_ids[0]); + + for (i = 1; i < api_params->num_game_ids; i++) { + int chars = snprintf(num, sizeof(num), "%u", api_params->game_ids[i]); + rc_url_builder_append(&builder, ",", 1); + rc_url_builder_append(&builder, num, chars); + } + + request->post_data = rc_url_builder_finalize(&builder); + request->content_type = RC_CONTENT_TYPE_URLENCODED; + + return builder.result; +} + +int 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_api_game_title_entry_t* entry; + rc_json_iterator_t iterator; + 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("Response") + }; + + rc_json_field_t entry_fields[] = { + RC_JSON_NEW_FIELD("ID"), + RC_JSON_NEW_FIELD("Title"), + RC_JSON_NEW_FIELD("ImageIcon") + }; + + 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 (!rc_json_get_required_array(&response->num_entries, &array_field, &response->response, &fields[2], "Response")) + return RC_MISSING_VALUE; + + if (response->num_entries) { + response->entries = (rc_api_game_title_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_entries * sizeof(rc_api_game_title_entry_t)); + if (!response->entries) + return RC_OUT_OF_MEMORY; + + memset(&iterator, 0, sizeof(iterator)); + iterator.json = array_field.value_start; + iterator.end = array_field.value_end; + + entry = response->entries; + while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) { + if (!rc_json_get_required_unum(&entry->id, &response->response, &entry_fields[0], "ID")) + return RC_MISSING_VALUE; + if (!rc_json_get_required_string(&entry->title, &response->response, &entry_fields[1], "Title")) + return RC_MISSING_VALUE; + + /* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */ + rc_json_extract_filename(&entry_fields[2]); + if (!rc_json_get_required_string(&entry->image_name, &response->response, &entry_fields[2], "ImageIcon")) + return RC_MISSING_VALUE; + + ++entry; + } + } + + return RC_OK; +} + +void rc_api_destroy_fetch_game_titles_response(rc_api_fetch_game_titles_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 4f3bc5b57c..1a183f214e 100644 --- a/deps/rcheevos/src/rapi/rc_api_runtime.c +++ b/deps/rcheevos/src/rapi/rc_api_runtime.c @@ -111,7 +111,6 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon rc_api_leaderboard_definition_t* leaderboard; rc_json_field_t array_field; rc_json_iterator_t iterator; - const char* str; const char* last_author = ""; const char* last_author_field = ""; size_t last_author_len = 0; @@ -180,17 +179,7 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon return RC_MISSING_VALUE; /* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */ - if (patchdata_fields[3].value_end) { - str = patchdata_fields[3].value_end - 5; - if (memcmp(str, ".png\"", 5) == 0) { - patchdata_fields[3].value_end -= 5; - - while (str > patchdata_fields[3].value_start && str[-1] != '/') - --str; - - patchdata_fields[3].value_start = str; - } - } + rc_json_extract_filename(&patchdata_fields[3]); rc_json_get_optional_string(&response->image_name, &response->response, &patchdata_fields[3], "ImageIcon", ""); /* estimate the amount of space necessary to store the rich presence script, achievements, and leaderboards. @@ -248,9 +237,15 @@ int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_respon if (!rc_json_get_required_string(&achievement->author, &response->response, &achievement_fields[6], "Author")) return RC_MISSING_VALUE; - last_author = achievement->author; - last_author_field = achievement_fields[6].value_start; - last_author_len = len; + 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")) diff --git a/deps/rcheevos/src/rc_client.c b/deps/rcheevos/src/rc_client.c index a6a61eb7bf..db8ab8bccd 100644 --- a/deps/rcheevos/src/rc_client.c +++ b/deps/rcheevos/src/rc_client.c @@ -78,6 +78,7 @@ typedef struct rc_client_load_state_t #endif } 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_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); @@ -884,7 +885,7 @@ void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_g } #endif - if (!client->game) + if (!rc_client_is_game_loaded(client)) return; rc_mutex_lock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */ @@ -1984,12 +1985,101 @@ static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* s rc_api_destroy_fetch_game_data_response(&fetch_game_data_response); } -static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) +static rc_client_game_info_t* rc_client_allocate_game(void) +{ + rc_client_game_info_t* game = (rc_client_game_info_t*)calloc(1, sizeof(*game)); + if (!game) + return NULL; + + rc_buffer_init(&game->buffer); + rc_runtime_init(&game->runtime); + + return game; +} + +static int rc_client_attach_load_state(rc_client_t* client, rc_client_load_state_t* load_state) +{ + if (client->state.load == NULL) { + rc_client_unload_game(client); + client->state.load = load_state; + + if (load_state->game == NULL) { + load_state->game = rc_client_allocate_game(); + if (!load_state->game) { + if (load_state->callback) + load_state->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, load_state->callback_userdata); + + return 0; + } + } + } + else if (client->state.load != load_state) { + /* previous load was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + + return 0; + } + + return 1; +} + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + +static void rc_client_external_load_state_callback(int result, const char* error_message, rc_client_t* client, void* userdata) +{ + rc_client_load_state_t* load_state = (rc_client_load_state_t*)userdata; + int async_aborted; + + client = load_state->client; + async_aborted = rc_client_end_async(client, &load_state->async_handle); + if (async_aborted) { + if (async_aborted != RC_CLIENT_ASYNC_DESTROYED) { + RC_CLIENT_LOG_VERBOSE(client, "Load aborted during external loading"); + } + + rc_client_unload_game(client); /* unload the game from the external client */ + rc_client_free_load_state(load_state); + return; + } + + if (result != RC_OK) { + rc_client_load_error(load_state, result, error_message); + return; + } + + rc_mutex_lock(&client->state.mutex); + load_state->progress = (client->state.load == load_state) ? + RC_CLIENT_LOAD_GAME_STATE_DONE : RC_CLIENT_LOAD_GAME_STATE_ABORTED; + client->state.load = NULL; + rc_mutex_unlock(&client->state.mutex); + + if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_DONE) { + /* previous load state was aborted */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else { + /* 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(); + load_state->game->public_.console_id = info->console_id; + client->game = load_state->game; + load_state->game = NULL; + } + + if (load_state->callback) + load_state->callback(RC_OK, NULL, client, load_state->callback_userdata); + } + + rc_client_free_load_state(load_state); +} + +#endif + +static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state) { - rc_api_fetch_game_data_request_t fetch_game_data_request; rc_client_t* client = load_state->client; - rc_api_request_t request; - int result; if (load_state->hash->game_id == 0) { #ifdef RC_CLIENT_SUPPORTS_HASH @@ -2058,20 +2148,35 @@ static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) #endif /* RC_CLIENT_SUPPORTS_HASH */ if (load_state->hash->game_id == 0) { - rc_client_subset_info_t* subset; +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + 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); + rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game"); + return; + } + /* no external method specifically for unknown game, just pass the hash through to begin_load_game below */ + } + else { +#endif + /* mimics rc_client_load_unknown_game without allocating a new game object */ + 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_.title = ""; + 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; + 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; - rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game"); - return; + rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game"); + return; +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + } +#endif } } @@ -2083,6 +2188,60 @@ static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) /* 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) + client->state.external_client->add_game_hash(load_state->hash->hash, load_state->hash->game_id); + + if (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); + } + return; + } +#endif + + rc_client_begin_fetch_game_data(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 = ""; + game->public_.console_id = RC_CONSOLE_UNKNOWN; + + if (strlen(tried_hashes) == 32) { /* only one hash tried, add it to the list */ + rc_client_game_hash_t* game_hash = rc_client_find_game_hash(client, tried_hashes); + game_hash->game_id = 0; + game->public_.hash = game_hash->hash; + } + else { + game->public_.hash = rc_buffer_strcpy(&game->buffer, tried_hashes); + } + + rc_client_unload_game(client); + client->game = game; +} + +static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state) +{ + rc_api_fetch_game_data_request_t fetch_game_data_request; + rc_client_t* client = load_state->client; + rc_api_request_t request; + int result; + rc_mutex_lock(&client->state.mutex); result = client->state.user; if (result == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) @@ -2160,7 +2319,7 @@ static void rc_client_identify_game_callback(const rc_api_server_response_t* ser /* previous load state was aborted, load_state was free'd */ } else { - rc_client_begin_fetch_game_data(load_state); + rc_client_process_resolved_hash(load_state); } } @@ -2193,35 +2352,25 @@ rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* return game_hash; } +void rc_client_add_game_hash(rc_client_t* client, const char* hash, uint32_t game_id) +{ + /* store locally, even if passing to external client */ + rc_client_game_hash_t* game_hash = rc_client_find_game_hash(client, hash); + game_hash->game_id = game_id; + +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + if (client->state.external_client && client->state.external_client->add_game_hash) + client->state.external_client->add_game_hash(hash, game_id); +#endif +} + static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, const char* hash, const char* file_path) { rc_client_t* client = load_state->client; rc_client_game_hash_t* old_hash; - if (client->state.load == NULL) { - rc_client_unload_game(client); - client->state.load = load_state; - - if (load_state->game == NULL) { - load_state->game = (rc_client_game_info_t*)calloc(1, sizeof(*load_state->game)); - if (!load_state->game) { - if (load_state->callback) - load_state->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, load_state->callback_userdata); - - rc_client_free_load_state(load_state); - return NULL; - } - - rc_buffer_init(&load_state->game->buffer); - rc_runtime_init(&load_state->game->runtime); - } - } - else if (client->state.load != load_state) { - /* previous load was aborted */ - if (load_state->callback) - load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); - + if (!rc_client_attach_load_state(client, load_state)) { rc_client_free_load_state(load_state); return NULL; } @@ -2265,7 +2414,7 @@ static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* loa else { RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash); - rc_client_begin_fetch_game_data(load_state); + rc_client_process_resolved_hash(load_state); } return (client->state.load == load_state) ? &load_state->async_handle : NULL; @@ -2327,8 +2476,12 @@ rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* cl } #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->begin_identify_and_load_game) - return client->state.external_client->begin_identify_and_load_game(client, console_id, file_path, data, data_size, callback, callback_userdata); + /* if a add_game_hash handler exists, do the identification locally, then pass the + * resulting game_id/hash to the external client */ + if (client->state.external_client && !client->state.external_client->add_game_hash) { + if (client->state.external_client->begin_identify_and_load_game) + return client->state.external_client->begin_identify_and_load_game(client, console_id, file_path, data, data_size, callback, callback_userdata); + } #endif if (data) { @@ -2471,6 +2624,13 @@ void rc_client_unload_game(rc_client_t* client) #ifdef RC_CLIENT_SUPPORTS_EXTERNAL if (client->state.external_client && client->state.external_client->unload_game) { client->state.external_client->unload_game(); + + /* a game object may have been allocated to manage hashes */ + game = client->game; + client->game = NULL; + if (game != NULL) + rc_client_free_game(game); + return; } #endif @@ -2629,15 +2789,77 @@ static rc_client_async_handle_t* rc_client_begin_change_media_internal(rc_client return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL; } +static rc_client_game_info_t* rc_client_check_pending_media(rc_client_t* client, const rc_client_pending_media_t* media) +{ + rc_client_game_info_t* game; + rc_client_pending_media_t* pending_media = NULL; + + rc_mutex_lock(&client->state.mutex); + if (client->state.load) { + game = client->state.load->game; + if (!game || game->public_.console_id == 0) { + /* still waiting for game data */ + pending_media = client->state.load->pending_media; + if (pending_media) + rc_client_free_pending_media(pending_media); + + pending_media = (rc_client_pending_media_t*)malloc(sizeof(*pending_media)); + if (!pending_media) { + rc_mutex_unlock(&client->state.mutex); + media->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, media->callback_userdata); + return NULL; + } + + memcpy(pending_media, media, sizeof(*pending_media)); + if (media->hash) + pending_media->hash = strdup(media->hash); + +#ifdef RC_CLIENT_SUPPORTS_HASH + if (media->file_path) + pending_media->file_path = strdup(media->file_path); + + if (media->data && media->data_size) { + pending_media->data = (uint8_t*)malloc(media->data_size); + if (!pending_media->data) { + rc_mutex_unlock(&client->state.mutex); + media->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, media->callback_userdata); + return NULL; + } + memcpy(pending_media->data, media->data, media->data_size); + } else { + pending_media->data = NULL; + } +#endif + + client->state.load->pending_media = pending_media; + } + } + else { + game = client->game; + } + rc_mutex_unlock(&client->state.mutex); + + if (!game) { + media->callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, media->callback_userdata); + return NULL; + } + + /* still waiting for game data - don't call callback - it's queued */ + if (pending_media) + return NULL; + + return game; +} + #ifdef RC_CLIENT_SUPPORTS_HASH rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path, const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata) { + rc_client_pending_media_t media; rc_client_game_hash_t* game_hash = NULL; - rc_client_media_hash_t* media_hash; rc_client_game_info_t* game; - rc_client_pending_media_t* pending_media = NULL; + rc_client_media_hash_t* media_hash; uint32_t path_djb2; if (!client) { @@ -2651,55 +2873,21 @@ 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) - 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_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); + } #endif - rc_mutex_lock(&client->state.mutex); - if (client->state.load) { - game = client->state.load->game; - if (game->public_.console_id == 0) { - /* still waiting for game data */ - pending_media = client->state.load->pending_media; - if (pending_media) - rc_client_free_pending_media(pending_media); + memset(&media, 0, sizeof(media)); + media.file_path = file_path; + media.data = (uint8_t*)data; + media.data_size = data_size; + media.callback = callback; + media.callback_userdata = callback_userdata; - pending_media = (rc_client_pending_media_t*)calloc(1, sizeof(*pending_media)); - if (!pending_media) { - rc_mutex_unlock(&client->state.mutex); - callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); - return NULL; - } - - pending_media->file_path = strdup(file_path); - pending_media->callback = callback; - pending_media->callback_userdata = callback_userdata; - if (data && data_size) { - pending_media->data_size = data_size; - pending_media->data = (uint8_t*)malloc(data_size); - if (!pending_media->data) { - rc_mutex_unlock(&client->state.mutex); - callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); - return NULL; - } - memcpy(pending_media->data, data, data_size); - } - - client->state.load->pending_media = pending_media; - } - } - else { - game = client->game; - } - rc_mutex_unlock(&client->state.mutex); - - if (!game) { - callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); - return NULL; - } - - /* still waiting for game data */ - if (pending_media) + game = rc_client_check_pending_media(client, &media); + if (game == NULL) return NULL; /* check to see if we've already hashed this file */ @@ -2749,11 +2937,25 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons rc_mutex_unlock(&client->state.mutex); 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); +#endif + rc_client_change_media_internal(client, game_hash, callback, callback_userdata); return NULL; } } +#ifdef RC_CLIENT_SUPPORTS_EXTERNAL + 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); + } +#endif + return rc_client_begin_change_media_internal(client, game, game_hash, callback, callback_userdata); } @@ -2762,9 +2964,9 @@ rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, cons rc_client_async_handle_t* rc_client_begin_change_media_from_hash(rc_client_t* client, const char* hash, rc_client_callback_t callback, void* callback_userdata) { + rc_client_pending_media_t media; rc_client_game_hash_t* game_hash; rc_client_game_info_t* game; - rc_client_pending_media_t* pending_media = NULL; if (!client) { callback(RC_INVALID_STATE, "client is required", client, callback_userdata); @@ -2782,40 +2984,13 @@ rc_client_async_handle_t* rc_client_begin_change_media_from_hash(rc_client_t* cl } #endif - rc_mutex_lock(&client->state.mutex); - if (client->state.load) { - game = client->state.load->game; - if (game->public_.console_id == 0) { - /* still waiting for game data */ - pending_media = client->state.load->pending_media; - if (pending_media) - rc_client_free_pending_media(pending_media); + memset(&media, 0, sizeof(media)); + media.hash = hash; + media.callback = callback; + media.callback_userdata = callback_userdata; - pending_media = (rc_client_pending_media_t*)calloc(1, sizeof(*pending_media)); - if (!pending_media) { - rc_mutex_unlock(&client->state.mutex); - callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata); - return NULL; - } - - pending_media->hash = strdup(hash); - pending_media->callback = callback; - pending_media->callback_userdata = callback_userdata; - - client->state.load->pending_media = pending_media; - } - } else { - game = client->game; - } - rc_mutex_unlock(&client->state.mutex); - - if (!game) { - callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); - return NULL; - } - - /* still waiting for game data */ - if (pending_media) + game = rc_client_check_pending_media(client, &media); + if (game == NULL) return NULL; /* check to see if we've already hashed this file. */ @@ -2861,7 +3036,7 @@ rc_client_async_handle_t* rc_client_begin_load_subset(rc_client_t* client, uint3 return client->state.external_client->begin_load_subset(client, subset_id, callback, callback_userdata); #endif - if (!client->game) { + if (!rc_client_is_game_loaded(client)) { callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata); return NULL; } @@ -2882,7 +3057,7 @@ rc_client_async_handle_t* rc_client_begin_load_subset(rc_client_t* client, uint3 load_state->hash->game_id = subset_id; client->state.load = load_state; - rc_client_begin_fetch_game_data(load_state); + rc_client_process_resolved_hash(load_state); return (client->state.load == load_state) ? &load_state->async_handle : NULL; } @@ -3570,9 +3745,11 @@ static void rc_client_award_achievement(rc_client_t* client, rc_client_achieveme callback_data->client = client; callback_data->id = achievement->public_.id; callback_data->hardcore = client->state.hardcore; - callback_data->game_hash = client->game->public_.hash; callback_data->unlock_time = achievement->public_.unlock_time; + if (client->game) /* may be NULL if this gets called while unloading the game (from another thread - events are raised outside the lock) */ + callback_data->game_hash = client->game->public_.hash; + RC_CLIENT_LOG_INFO_FORMATTED(client, "Awarding achievement %u: %s", achievement->public_.id, achievement->public_.title); rc_client_award_achievement_server_call(callback_data); } @@ -4311,6 +4488,7 @@ static void rc_client_fetch_leaderboard_entries_callback(const rc_api_server_res } list->num_entries = lbinfo_response.num_entries; + list->total_entries = lbinfo_response.total_entries; lbinfo_callback_data->callback(RC_OK, NULL, list, client, lbinfo_callback_data->callback_userdata); } @@ -4797,6 +4975,8 @@ static void rc_client_progress_tracker_timer_elapsed(rc_client_scheduled_callbac static void rc_client_do_frame_update_progress_tracker(rc_client_t* client, rc_client_game_info_t* game) { + /* ASSERT: this should only be called if the mutex is held */ + if (!game->progress_tracker.hide_callback) { game->progress_tracker.hide_callback = (rc_client_scheduled_callback_data_t*) rc_buffer_alloc(&game->buffer, sizeof(rc_client_scheduled_callback_data_t)); @@ -5181,6 +5361,7 @@ void rc_client_idle(rc_client_t* client) else { /* remove the callback from the queue while we process it. callback can requeue if desired */ client->state.scheduled_callbacks = scheduled_callback->next; + scheduled_callback->next = NULL; } } rc_mutex_unlock(&client->state.mutex); @@ -5256,7 +5437,7 @@ static void rc_client_reschedule_callback(rc_client_t* client, continue; } - if (!next || when < next->when) { + if (!next || (when < next->when && when != 0)) { /* insert here */ callback->next = next; *last = callback; @@ -5411,7 +5592,7 @@ int rc_client_serialize_progress_sized(rc_client_t* client, uint8_t* buffer, siz return client->state.external_client->serialize_progress(buffer, buffer_size); #endif - if (!client->game) + if (!rc_client_is_game_loaded(client)) return RC_NO_GAME_LOADED; if (!buffer) @@ -5535,7 +5716,7 @@ int rc_client_deserialize_progress_sized(rc_client_t* client, const uint8_t* ser return client->state.external_client->deserialize_progress(serialized, serialized_size); #endif - if (!client->game) + if (!rc_client_is_game_loaded(client)) return RC_NO_GAME_LOADED; rc_mutex_lock(&client->state.mutex); diff --git a/deps/rcheevos/src/rc_client_external.h b/deps/rcheevos/src/rc_client_external.h index 82ec1d49d7..520a883e13 100644 --- a/deps/rcheevos/src/rc_client_external.h +++ b/deps/rcheevos/src/rc_client_external.h @@ -38,6 +38,7 @@ typedef const rc_client_subset_t* (RC_CCONV *rc_client_external_get_subset_info_ typedef void (RC_CCONV *rc_client_external_get_user_game_summary_func_t)(rc_client_user_game_summary_t* summary); typedef rc_client_async_handle_t* (RC_CCONV *rc_client_external_begin_change_media_func_t)(rc_client_t* client, const char* file_path, const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata); +typedef void (RC_CCONV* rc_client_external_add_game_hash_func_t)(const char* hash, uint32_t game_id); /* NOTE: rc_client_external_create_achievement_list_func_t returns an internal wrapper structure which contains the public list * and a destructor function. */ @@ -124,9 +125,16 @@ typedef struct rc_client_external_t rc_client_external_serialize_progress_func_t serialize_progress; rc_client_external_deserialize_progress_func_t deserialize_progress; + /* VERSION 2 */ + rc_client_external_add_game_hash_func_t add_game_hash; + rc_client_external_set_string_func_t load_unknown_game; + } rc_client_external_t; -#define RC_CLIENT_EXTERNAL_VERSION 1 +#define RC_CLIENT_EXTERNAL_VERSION 2 + +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); RC_END_C_DECLS diff --git a/deps/rcheevos/src/rc_libretro.c b/deps/rcheevos/src/rc_libretro.c index d94d6d5b4c..14398ef1ec 100644 --- a/deps/rcheevos/src/rc_libretro.c +++ b/deps/rcheevos/src/rc_libretro.c @@ -69,6 +69,7 @@ static const rc_disallowed_setting_t _rc_disallowed_ecwolf_settings[] = { static const rc_disallowed_setting_t _rc_disallowed_fbneo_settings[] = { { "fbneo-allow-patched-romsets", "enabled" }, { "fbneo-cheat-*", "!,Disabled,0 - Disabled" }, + { "fbneo-cpu-speed-adjust", "??%" }, /* disallow speeds under 100% */ { "fbneo-dipswitch-*", "Universe BIOS*" }, { "fbneo-neogeo-mode", "UNIBIOS" }, { NULL, NULL } @@ -178,7 +179,7 @@ static const rc_disallowed_core_settings_t rc_disallowed_core_settings[] = { static int rc_libretro_string_equal_nocase_wildcard(const char* test, const char* value) { char c1, c2; while ((c1 = *test++)) { - if (tolower(c1) != tolower(c2 = *value++)) + if (tolower(c1) != tolower(c2 = *value++) && c2 != '?') return (c2 == '*'); } diff --git a/deps/rcheevos/src/rc_util.c b/deps/rcheevos/src/rc_util.c index edc6a5a398..b6aa5bf6d6 100644 --- a/deps/rcheevos/src/rc_util.c +++ b/deps/rcheevos/src/rc_util.c @@ -184,6 +184,8 @@ const char* rc_error_str(int ret) case RC_INVALID_CREDENTIALS: return "Invalid credentials"; case RC_EXPIRED_TOKEN: return "Expired token"; 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"; default: return "Unknown error"; } } diff --git a/deps/rcheevos/src/rc_version.h b/deps/rcheevos/src/rc_version.h index 6d9614faf7..7ed7301d27 100644 --- a/deps/rcheevos/src/rc_version.h +++ b/deps/rcheevos/src/rc_version.h @@ -8,7 +8,7 @@ RC_BEGIN_C_DECLS #define RCHEEVOS_VERSION_MAJOR 11 -#define RCHEEVOS_VERSION_MINOR 2 +#define RCHEEVOS_VERSION_MINOR 5 #define RCHEEVOS_VERSION_PATCH 0 #define RCHEEVOS_MAKE_VERSION(major, minor, patch) (major * 1000000 + minor * 1000 + patch) diff --git a/deps/rcheevos/src/rcheevos/condition.c b/deps/rcheevos/src/rcheevos/condition.c index 2e7ccce1cb..de8efd924f 100644 --- a/deps/rcheevos/src/rcheevos/condition.c +++ b/deps/rcheevos/src/rcheevos/condition.c @@ -143,6 +143,14 @@ static int rc_parse_operator(const char** memaddr) { ++(*memaddr); return RC_OPERATOR_MOD; + case '+': + ++(*memaddr); + return RC_OPERATOR_ADD; + + case '-': + ++(*memaddr); + return RC_OPERATOR_SUB; + case '\0':/* end of string */ case '_': /* next condition */ case 'S': /* next condset */ @@ -180,12 +188,13 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse case 'q': case 'Q': self->type = RC_CONDITION_MEASURED_IF; break; case 'i': case 'I': self->type = RC_CONDITION_ADD_ADDRESS; can_modify = 1; break; case 't': case 'T': self->type = RC_CONDITION_TRIGGER; break; + case 'k': case 'K': self->type = RC_CONDITION_REMEMBER; can_modify = 1; break; case 'z': case 'Z': self->type = RC_CONDITION_RESET_NEXT_IF; break; case 'g': case 'G': parse->measured_as_percent = 1; self->type = RC_CONDITION_MEASURED; break; - /* e f h j k l s u v w x y */ + /* e f h j l s u v w x y */ default: parse->offset = RC_INVALID_CONDITION_TYPE; return 0; } @@ -231,6 +240,8 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse 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) break; @@ -243,6 +254,7 @@ rc_condition_t* rc_parse_condition(const char** memaddr, rc_parse_state_t* parse 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; @@ -560,5 +572,14 @@ void rc_evaluate_condition_value(rc_typed_value_t* value, rc_condition_t* self, 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; } } diff --git a/deps/rcheevos/src/rcheevos/condset.c b/deps/rcheevos/src/rcheevos/condset.c index 2d5d505b62..f03d47b452 100644 --- a/deps/rcheevos/src/rcheevos/condset.c +++ b/deps/rcheevos/src/rcheevos/condset.c @@ -53,6 +53,7 @@ rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, in 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) */ break; @@ -88,6 +89,8 @@ rc_condset_t* rc_parse_condset(const char** memaddr, rc_parse_state_t* parse, in 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; @@ -222,6 +225,15 @@ static int rc_test_condset_internal(rc_condset_t* self, int processing_pause, rc 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 */ @@ -417,6 +429,10 @@ int rc_test_condset(rc_condset_t* self, rc_eval_state_t* eval_state) { return 1; } + /* 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->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); diff --git a/deps/rcheevos/src/rcheevos/consoleinfo.c b/deps/rcheevos/src/rcheevos/consoleinfo.c index 90f0020f2f..b8bee226d4 100644 --- a/deps/rcheevos/src/rcheevos/consoleinfo.c +++ b/deps/rcheevos/src/rcheevos/consoleinfo.c @@ -368,9 +368,14 @@ static const rc_memory_regions_t rc_memory_regions_atari_lynx = { _rc_memory_reg /* ===== ColecoVision ===== */ static const rc_memory_region_t _rc_memory_regions_colecovision[] = { - { 0x000000U, 0x0003FFU, 0x006000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } + /* "System RAM" refers to the main RAM at 0x6000-0x63FF. However, this RAM might not always be visible. + * If the Super Game Module (SGM) is active, then it might overlay its own RAM at 0x0000-0x1FFF and 0x2000-0x7FFF. + * These positions overlap the BIOS and System RAM, therefore we use virtual addresses for these memory spaces. */ + { 0x000000U, 0x0003FFU, 0x006000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x000400U, 0x0023FFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "SGM Low RAM" }, /* Normally situated at 0x0000-0x1FFF, which overlaps the BIOS */ + { 0x002400U, 0x0083FFU, 0x012000U, RC_MEMORY_TYPE_SYSTEM_RAM, "SGM High RAM" } /* Normally situated at 0x2000-0x7FFF, which overlaps System RAM */ }; -static const rc_memory_regions_t rc_memory_regions_colecovision = { _rc_memory_regions_colecovision, 1 }; +static const rc_memory_regions_t rc_memory_regions_colecovision = { _rc_memory_regions_colecovision, 3 }; /* ===== Commodore 64 ===== */ /* https://www.c64-wiki.com/wiki/Memory_Map */ @@ -770,10 +775,11 @@ static const rc_memory_regions_t rc_memory_regions_pcfx = { _rc_memory_regions_p /* ===== PlayStation ===== */ /* http://www.raphnet.net/electronique/psx_adaptor/Playstation.txt */ static const rc_memory_region_t _rc_memory_regions_playstation[] = { - { 0x000000U, 0x00FFFFU, 0x000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, - { 0x010000U, 0x1FFFFFU, 0x010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } + { 0x000000U, 0x00FFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Kernel RAM" }, + { 0x010000U, 0x1FFFFFU, 0x00010000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x200000U, 0x2003FFU, 0x1F800000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Scratchpad RAM" } }; -static const rc_memory_regions_t rc_memory_regions_playstation = { _rc_memory_regions_playstation, 2 }; +static const rc_memory_regions_t rc_memory_regions_playstation = { _rc_memory_regions_playstation, 3 }; /* ===== PlayStation 2 ===== */ /* https://psi-rockin.github.io/ps2tek/ */ diff --git a/deps/rcheevos/src/rcheevos/operand.c b/deps/rcheevos/src/rcheevos/operand.c index 65d41dc5e0..09ac65ee19 100644 --- a/deps/rcheevos/src/rcheevos/operand.c +++ b/deps/rcheevos/src/rcheevos/operand.c @@ -3,6 +3,7 @@ #include #include #include +#include #ifndef RC_DISABLE_LUA @@ -64,6 +65,37 @@ static int rc_parse_operand_lua(rc_operand_t* self, const char** memaddr, rc_par return RC_OK; } +static int rc_parse_operand_variable(rc_operand_t* self, const char** memaddr) { + const char* aux = *memaddr; + size_t i; + char varName[RC_VALUE_MAX_NAME_LENGTH + 1] = { 0 }; + + for (i = 0; i < RC_VALUE_MAX_NAME_LENGTH && *aux != '}'; i++) { + if (!rc_is_valid_variable_character(*aux, i == 0)) + return RC_INVALID_VARIABLE_NAME; + + varName[i] = *aux++; + } + + if (i == 0) + return RC_INVALID_VARIABLE_NAME; + + if (*aux != '}') + return RC_INVALID_VARIABLE_NAME; + + ++aux; + + if (strcmp(varName, "recall") == 0) { + self->type = RC_OPERAND_RECALL; + } + else { /* process named variable when feature is available.*/ + return RC_UNKNOWN_VARIABLE_NAME; + } + + *memaddr = aux; + 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) { const char* aux = *memaddr; uint32_t address; @@ -231,6 +263,13 @@ int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indire self->value.num = (unsigned)value; } break; + case '{': /* variable */ + ++aux; + ret = rc_parse_operand_variable(self, &aux); + if (ret < 0) + return ret; + + break; case '0': if (aux[1] == 'x' || aux[1] == 'X') { /* hex integer constant */ @@ -313,6 +352,7 @@ int rc_operand_is_memref(const rc_operand_t* self) { case RC_OPERAND_CONST: case RC_OPERAND_FP: case RC_OPERAND_LUA: + case RC_OPERAND_RECALL: return 0; default: @@ -320,6 +360,16 @@ int rc_operand_is_memref(const rc_operand_t* self) { } } +int rc_operand_is_recall(const rc_operand_t* self) { + switch (self->type) { + case RC_OPERAND_RECALL: + return 1; + + default: + return 0; + } +} + int rc_operand_is_float(const rc_operand_t* self) { if (self->type == RC_OPERAND_FP) return 1; @@ -462,6 +512,11 @@ void rc_evaluate_operand(rc_typed_value_t* result, rc_operand_t* self, rc_eval_s break; + case RC_OPERAND_RECALL: + result->type = eval_state->recall_value.type; + result->value = eval_state->recall_value.value; + return; + default: result->type = RC_VALUE_TYPE_UNSIGNED; result->value.u32 = rc_get_memref_value(self->value.memref, self->type, eval_state); diff --git a/deps/rcheevos/src/rcheevos/rc_internal.h b/deps/rcheevos/src/rcheevos/rc_internal.h index 39154cd685..fa913fec84 100644 --- a/deps/rcheevos/src/rcheevos/rc_internal.h +++ b/deps/rcheevos/src/rcheevos/rc_internal.h @@ -89,12 +89,13 @@ typedef struct { void* peek_userdata; lua_State* L; - rc_typed_value_t measured_value; /* Measured */ - 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 */ - uint8_t measured_from_hits; /* true if the measured_value came from a condition's hit count */ - uint8_t was_cond_reset; /* ResetNextIf triggered */ + 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 */ + uint8_t measured_from_hits; /* true if the measured_value came from a condition's hit count */ + uint8_t was_cond_reset; /* ResetNextIf triggered */ } rc_eval_state_t; @@ -169,7 +170,9 @@ int rc_parse_operand(rc_operand_t* self, const char** memaddr, uint8_t is_indire void rc_evaluate_operand(rc_typed_value_t* value, rc_operand_t* self, rc_eval_state_t* eval_state); int rc_operand_is_float_memref(const rc_operand_t* self); int rc_operand_is_float(const rc_operand_t* self); +int rc_operand_is_recall(const rc_operand_t* self); +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); void rc_reset_value(rc_value_t* self); diff --git a/deps/rcheevos/src/rcheevos/runtime.c b/deps/rcheevos/src/rcheevos/runtime.c index 09c3aee217..25e9885dcf 100644 --- a/deps/rcheevos/src/rcheevos/runtime.c +++ b/deps/rcheevos/src/rcheevos/runtime.c @@ -10,7 +10,7 @@ #define RC_RICHPRESENCE_DISPLAY_BUFFER_SIZE 256 rc_runtime_t* rc_runtime_alloc(void) { - rc_runtime_t* self = (rc_runtime_t*)malloc(sizeof(rc_runtime_t)); + rc_runtime_t* self = malloc(sizeof(rc_runtime_t)); if (self) { rc_runtime_init(self); @@ -834,7 +834,7 @@ void rc_runtime_invalidate_address(rc_runtime_t* self, uint32_t address) { } } -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; diff --git a/deps/rcheevos/src/rcheevos/runtime_progress.c b/deps/rcheevos/src/rcheevos/runtime_progress.c index 07e1dbe1b6..629f0e376f 100644 --- a/deps/rcheevos/src/rcheevos/runtime_progress.c +++ b/deps/rcheevos/src/rcheevos/runtime_progress.c @@ -210,6 +210,7 @@ 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: return 0; diff --git a/deps/rcheevos/src/rcheevos/value.c b/deps/rcheevos/src/rcheevos/value.c index f7da42d804..25f2204e39 100644 --- a/deps/rcheevos/src/rcheevos/value.c +++ b/deps/rcheevos/src/rcheevos/value.c @@ -5,6 +5,20 @@ #include /* FLT_EPSILON */ #include /* fmod */ + + +int rc_is_valid_variable_character(char ch, int is_first) { + if (is_first) { + if (!isalpha((unsigned char)ch)) + return 0; + } + else { + if (!isalnum((unsigned char)ch)) + return 0; + } + return 1; +} + static void rc_parse_cond_value(rc_value_t* self, const char** memaddr, rc_parse_state_t* parse) { rc_condset_t** next_clause; @@ -114,6 +128,8 @@ void rc_parse_legacy_value(rc_value_t* self, const char** memaddr, rc_parse_stat 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; diff --git a/deps/rcheevos/src/rhash/hash.c b/deps/rcheevos/src/rhash/hash.c index 5e2f19628a..715f695c73 100644 --- a/deps/rcheevos/src/rhash/hash.c +++ b/deps/rcheevos/src/rhash/hash.c @@ -716,6 +716,12 @@ struct rc_hash_zip_idx 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; @@ -723,9 +729,12 @@ static int rc_hash_zip_idx_sort(const void* a, const void* b) return memcmp(A->data, B->data, len); } -static int rc_hash_zip_file(md5_state_t* md5, void* file_handle) +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; + 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; @@ -789,7 +798,7 @@ static int rc_hash_zip_file(md5_state_t* md5, void* file_handle) 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 */ + /* 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)) { @@ -837,7 +846,7 @@ static int rc_hash_zip_file(md5_state_t* md5, void* file_handle) hashindex = hashindices; /* Now process the central directory file records */ - for (i_file = 0, cdir = cdir_start; i_file < total_files && cdir >= cdir_start && cdir <= cdir_max; i_file++, cdir += cdir_entry_len) + 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); @@ -907,6 +916,27 @@ static int rc_hash_zip_file(md5_state_t* md5, void* file_handle) 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; @@ -951,6 +981,11 @@ static int rc_hash_zip_file(md5_state_t* md5, void* file_handle) 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 @@ -960,10 +995,86 @@ static int rc_hash_zip_file(md5_state_t* md5, void* file_handle) #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; - size_t path_len; int res; void* file_handle = rc_file_open(path); @@ -972,34 +1083,14 @@ static int rc_hash_ms_dos(char hash[33], const char* path) /* hash the main content zip file first */ md5_init(&md5); - res = rc_hash_zip_file(&md5, file_handle); + dosz.path = path; + dosz.child = NULL; + res = rc_hash_zip_file(&md5, file_handle, &dosz); rc_file_close(file_handle); if (!res) return 0; - /* if this is a .dosz file, check if an associated .dosc file exists */ - path_len = strlen(path); - if (path[path_len-1] == 'z' || path[path_len-1] == 'Z') - { - char *dosc_path = strdup(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] = (path[path_len-1] == 'z' ? 'c' : 'C'); - file_handle = rc_file_open(dosc_path); - free((void*)dosc_path); - if (file_handle) - { - res = rc_hash_zip_file(&md5, file_handle); - rc_file_close(file_handle); - - if (!res) - return 0; - } - } - return rc_hash_finalize(&md5, hash); } @@ -1008,11 +1099,12 @@ 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 + * https://github.com/libretro/FBNeo/blob/master/src/burner/libretro/README.md#emulating-consoles-and-computers */ if (filename > path + 1) { @@ -1029,31 +1121,67 @@ static int rc_hash_arcade(char hash[33], const char* path) } 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 || - memcmp(folder, "fds", 3) == 0 || - memcmp(folder, "sms", 3) == 0 || - memcmp(folder, "msx", 3) == 0 || - memcmp(folder, "ngp", 3) == 0 || - memcmp(folder, "pce", 3) == 0 || - memcmp(folder, "sgx", 3) == 0) + 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) + 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 || - memcmp(folder, "sg1000", 6) == 0) + 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 || - memcmp(folder, "megadriv", 8) == 0 || - memcmp(folder, "spectrum", 8) == 0) + 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: @@ -1062,10 +1190,8 @@ static int rc_hash_arcade(char hash[33], const char* path) if (include_folder) { - char buffer[128]; /* realistically, this should never need more than ~20 characters */ if (parent_folder_length + filename_length + 1 < sizeof(buffer)) { - memcpy(&buffer[0], folder, parent_folder_length); 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);