diff --git a/3rdparty/rcheevos/include/rc_api_runtime.h b/3rdparty/rcheevos/include/rc_api_runtime.h index 5b5552ca54..fef1b8769d 100644 --- a/3rdparty/rcheevos/include/rc_api_runtime.h +++ b/3rdparty/rcheevos/include/rc_api_runtime.h @@ -220,6 +220,8 @@ typedef struct rc_api_award_achievement_request_t { uint32_t hardcore; /* The hash associated to the game being played */ const char* game_hash; + /* The number of seconds since the achievement was unlocked */ + uint32_t seconds_since_unlock; } rc_api_award_achievement_request_t; @@ -263,6 +265,8 @@ typedef struct rc_api_submit_lboard_entry_request_t { int32_t score; /* The hash associated to the game being played */ const char* game_hash; + /* The number of seconds since the leaderboard attempt was completed */ + uint32_t seconds_since_completion; } rc_api_submit_lboard_entry_request_t; diff --git a/3rdparty/rcheevos/src/rapi/rc_api_runtime.c b/3rdparty/rcheevos/src/rapi/rc_api_runtime.c index 1a183f214e..0ad58f01fe 100644 --- a/3rdparty/rcheevos/src/rapi/rc_api_runtime.c +++ b/3rdparty/rcheevos/src/rapi/rc_api_runtime.c @@ -412,6 +412,8 @@ int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_ap rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0); if (api_params->game_hash && *api_params->game_hash) rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + if (api_params->seconds_since_unlock) + rc_url_builder_append_unum_param(&builder, "o", api_params->seconds_since_unlock); /* Evaluate the signature. */ md5_init(&md5); @@ -420,6 +422,14 @@ int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_ap md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); snprintf(buffer, sizeof(buffer), "%d", api_params->hardcore ? 1 : 0); md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + if (api_params->seconds_since_unlock) { + /* second achievement id is needed by delegated unlock. including it here allows overloading + * the hash generating code on the server */ + snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + snprintf(buffer, sizeof(buffer), "%u", api_params->seconds_since_unlock); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + } md5_finish(&md5, digest); rc_format_md5(buffer, digest); rc_url_builder_append_str_param(&builder, "v", buffer); @@ -505,6 +515,9 @@ int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_ if (api_params->game_hash && *api_params->game_hash) rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + if (api_params->seconds_since_completion) + rc_url_builder_append_unum_param(&builder, "o", api_params->seconds_since_completion); + /* Evaluate the signature. */ md5_init(&md5); snprintf(buffer, sizeof(buffer), "%u", api_params->leaderboard_id); @@ -512,6 +525,10 @@ int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_ md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username)); snprintf(buffer, sizeof(buffer), "%d", api_params->score); md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + if (api_params->seconds_since_completion) { + snprintf(buffer, sizeof(buffer), "%u", api_params->seconds_since_completion); + md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer)); + } md5_finish(&md5, digest); rc_format_md5(buffer, digest); rc_url_builder_append_str_param(&builder, "v", buffer); diff --git a/3rdparty/rcheevos/src/rc_client.c b/3rdparty/rcheevos/src/rc_client.c index fc09c7d346..822a59b0f4 100644 --- a/3rdparty/rcheevos/src/rc_client.c +++ b/3rdparty/rcheevos/src/rc_client.c @@ -578,13 +578,7 @@ static int rc_client_get_image_url(char buffer[], size_t buffer_size, int image_ image_request.image_name = image_name; result = rc_api_init_fetch_image_request(&request, &image_request); if (result == RC_OK) - { - const size_t url_length = strlen(request.url); - if (url_length >= buffer_size) - result = RC_INSUFFICIENT_BUFFER; - else - memcpy(buffer, request.url, url_length + 1); - } + snprintf(buffer, buffer_size, "%s", request.url); rc_api_destroy_request(&request); return result; @@ -1440,7 +1434,6 @@ static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_s 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) { @@ -1461,17 +1454,15 @@ static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_s start_session_response->num_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); } + /* make the loaded game active if another game is not aleady being loaded. */ rc_mutex_lock(&client->state.mutex); - if (client->state.load == NULL) + if (client->state.load == load_state) client->game = load_state->game; + else + load_state->progress = RC_CLIENT_LOAD_GAME_STATE_ABORTED; rc_mutex_unlock(&client->state.mutex); - if (client->game != load_state->game) { - /* previous load state was aborted */ - if (load_state->callback) - load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); - } - else { + if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_ABORTED) { /* if a change media request is pending, kick it off */ rc_client_pending_media_t* pending_media; @@ -1481,6 +1472,9 @@ static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_s rc_mutex_unlock(&load_state->client->state.mutex); if (pending_media) { + /* rc_client_check_pending_media will fail if it can't find the game in client->game or + * 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, pending_media->callback, pending_media->callback_userdata); @@ -1494,12 +1488,50 @@ static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_s rc_client_free_pending_media(pending_media); } - /* client->game must be set before calling this function so it can query the console_id */ + rc_mutex_lock(&client->state.mutex); + if (client->state.load != load_state) + load_state->progress = RC_CLIENT_LOAD_GAME_STATE_ABORTED; + rc_mutex_unlock(&client->state.mutex); + } + + /* 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); + rc_mutex_lock(&client->state.mutex); + if (client->state.load != load_state) + load_state->progress = RC_CLIENT_LOAD_GAME_STATE_ABORTED; + rc_mutex_unlock(&client->state.mutex); + } + + /* if the game is still being loaded, load any active acheivements/leaderboards into the runtime */ + if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_ABORTED) { rc_client_activate_achievements(load_state->game, client); rc_client_activate_leaderboards(load_state->game, client); + /* detach the load state to indicate that loading is fully complete */ + rc_mutex_lock(&client->state.mutex); + if (client->state.load == load_state) + client->state.load = NULL; + else + load_state->progress = RC_CLIENT_LOAD_GAME_STATE_ABORTED; + rc_mutex_unlock(&client->state.mutex); + } + + /* one last sanity check to make sure the game is still being loaded. */ + if (load_state->progress == RC_CLIENT_LOAD_GAME_STATE_ABORTED) { + /* game has been unloaded, or another game is being loaded over the top of this game */ + if (load_state->callback) + load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); + } + else { if (load_state->hash->hash[0] != '[') { if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_LOCKED) { /* schedule the periodic ping */ @@ -2007,7 +2039,6 @@ static int rc_client_attach_load_state(rc_client_t* client, rc_client_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(); @@ -2018,6 +2049,10 @@ static int rc_client_attach_load_state(rc_client_t* client, rc_client_load_state return 0; } } + + rc_mutex_lock(&client->state.mutex); + client->state.load = load_state; + rc_mutex_unlock(&client->state.mutex); } else if (client->state.load != load_state) { /* previous load was aborted */ @@ -2621,8 +2656,6 @@ static void rc_client_game_mark_ui_to_be_hidden(rc_client_t* client, rc_client_g void rc_client_unload_game(rc_client_t* client) { rc_client_game_info_t* game; - rc_client_scheduled_callback_data_t** last; - rc_client_scheduled_callback_data_t* next; if (!client) return; @@ -2649,29 +2682,38 @@ void rc_client_unload_game(rc_client_t* client) if (client->state.load) { /* this mimics rc_client_abort_async without nesting the lock */ client->state.load->async_handle.aborted = RC_CLIENT_ASYNC_ABORTED; + + /* if the game is still being loaded, let the load process clean it up */ + if (client->state.load->game == game) + game = NULL; + client->state.load = NULL; } if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_ON; - if (game != NULL) + if (game != NULL) { + rc_client_scheduled_callback_data_t** last; + rc_client_scheduled_callback_data_t* next; + rc_client_game_mark_ui_to_be_hidden(client, game); - last = &client->state.scheduled_callbacks; - do { - next = *last; - if (!next) - break; + last = &client->state.scheduled_callbacks; + do { + next = *last; + if (!next) + break; - /* remove rich presence ping scheduled event for game */ - if (next->callback == rc_client_ping && game && next->related_id == game->public_.id) { - *last = next->next; - continue; - } + /* remove rich presence ping scheduled event for game */ + if (next->callback == rc_client_ping && next->related_id == game->public_.id) { + *last = next->next; + continue; + } - last = &next->next; - } while (1); + last = &next->next; + } while (1); + } rc_mutex_unlock(&client->state.mutex); @@ -3534,7 +3576,7 @@ typedef struct rc_client_award_achievement_callback_data_t uint32_t retry_count; uint8_t hardcore; const char* game_hash; - time_t unlock_time; + rc_clock_t unlock_time; rc_client_t* client; rc_client_scheduled_callback_data_t* scheduled_callback_data; } rc_client_award_achievement_callback_data_t; @@ -3685,6 +3727,11 @@ static void rc_client_award_achievement_server_call(rc_client_award_achievement_ api_params.hardcore = ach_data->hardcore; api_params.game_hash = ach_data->game_hash; + if (ach_data->retry_count) { + const rc_clock_t now = ach_data->client->callbacks.get_time_millisecs(ach_data->client); + api_params.seconds_since_unlock = (uint32_t)((now - ach_data->unlock_time) / 1000); + } + result = rc_api_init_award_achievement_request(&request, &api_params); if (result != RC_OK) { RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error constructing unlock request for achievement %u: %s", ach_data->id, rc_error_str(result)); @@ -3751,7 +3798,8 @@ 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->unlock_time = achievement->public_.unlock_time; + callback_data->game_hash = client->game->public_.hash; + callback_data->unlock_time = client->callbacks.get_time_millisecs(client); 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; @@ -4185,7 +4233,7 @@ typedef struct rc_client_submit_leaderboard_entry_callback_data_t int32_t score; uint32_t retry_count; const char* game_hash; - time_t submit_time; + rc_clock_t submit_time; rc_client_t* client; rc_client_scheduled_callback_data_t* scheduled_callback_data; } rc_client_submit_leaderboard_entry_callback_data_t; @@ -4340,6 +4388,11 @@ static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_lead api_params.score = lboard_data->score; api_params.game_hash = lboard_data->game_hash; + if (lboard_data->retry_count) { + const rc_clock_t now = lboard_data->client->callbacks.get_time_millisecs(lboard_data->client); + api_params.seconds_since_completion = (uint32_t)((now - lboard_data->submit_time) / 1000); + } + result = rc_api_init_submit_lboard_entry_request(&request, &api_params); if (result != RC_OK) { RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error constructing submit leaderboard entry for leaderboard %u: %s", lboard_data->id, rc_error_str(result)); @@ -4383,7 +4436,7 @@ static void rc_client_submit_leaderboard_entry(rc_client_t* client, rc_client_le callback_data->id = leaderboard->public_.id; callback_data->score = leaderboard->value; callback_data->game_hash = client->game->public_.hash; - callback_data->submit_time = time(NULL); + callback_data->submit_time = client->callbacks.get_time_millisecs(client); RC_CLIENT_LOG_INFO_FORMATTED(client, "Submitting %s (%d) for leaderboard %u: %s", leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title); diff --git a/3rdparty/rcheevos/src/rc_libretro.c b/3rdparty/rcheevos/src/rc_libretro.c index 14398ef1ec..d343ce78ce 100644 --- a/3rdparty/rcheevos/src/rc_libretro.c +++ b/3rdparty/rcheevos/src/rc_libretro.c @@ -36,6 +36,17 @@ typedef struct rc_disallowed_core_settings_t const rc_disallowed_setting_t* disallowed_settings; } rc_disallowed_core_settings_t; + +static const rc_disallowed_setting_t _rc_disallowed_beetle_psx_settings[] = { + { "beetle_psx_cpu_freq_scale", "<100" }, + { NULL, NULL } +}; + +static const rc_disallowed_setting_t _rc_disallowed_beetle_psx_hw_settings[] = { + { "beetle_psx_hw_cpu_freq_scale", "<100" }, + { NULL, NULL } +}; + static const rc_disallowed_setting_t _rc_disallowed_bsnes_settings[] = { { "bsnes_region", "pal" }, { NULL, NULL } @@ -80,6 +91,11 @@ static const rc_disallowed_setting_t _rc_disallowed_fceumm_settings[] = { { NULL, NULL } }; +static const rc_disallowed_setting_t _rc_disallowed_flycast_settings[] = { + { "reicast_sh4clock", "<200" }, + { NULL, NULL } +}; + static const rc_disallowed_setting_t _rc_disallowed_gpgx_settings[] = { { "genesis_plus_gx_lock_on", ",action replay (pro),game genie" }, { "genesis_plus_gx_region_detect", "pal" }, @@ -108,6 +124,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_region", "pal" }, { NULL, NULL } }; @@ -140,6 +157,11 @@ static const rc_disallowed_setting_t _rc_disallowed_snes9x_settings[] = { { NULL, NULL } }; +static const rc_disallowed_setting_t _rc_disallowed_swanstation_settings[] = { + { "swanstation_CPU_Overclock", "<100" }, + { NULL, NULL } +}; + static const rc_disallowed_setting_t _rc_disallowed_vice_settings[] = { { "vice_autostart", "disabled" }, /* autostart dictates initial load and reset from menu */ { "vice_reset", "!autostart" }, /* reset dictates behavior when pressing reset button (END) */ @@ -152,6 +174,8 @@ static const rc_disallowed_setting_t _rc_disallowed_virtual_jaguar_settings[] = }; static const rc_disallowed_core_settings_t rc_disallowed_core_settings[] = { + { "Beetle PSX", _rc_disallowed_beetle_psx_settings }, + { "Beetle PSX HW", _rc_disallowed_beetle_psx_hw_settings }, { "bsnes-mercury", _rc_disallowed_bsnes_settings }, { "cap32", _rc_disallowed_cap32_settings }, { "dolphin-emu", _rc_disallowed_dolphin_settings }, @@ -160,6 +184,7 @@ static const rc_disallowed_core_settings_t rc_disallowed_core_settings[] = { { "ecwolf", _rc_disallowed_ecwolf_settings }, { "FCEUmm", _rc_disallowed_fceumm_settings }, { "FinalBurn Neo", _rc_disallowed_fbneo_settings }, + { "Flycast", _rc_disallowed_flycast_settings }, { "Genesis Plus GX", _rc_disallowed_gpgx_settings }, { "Genesis Plus GX Wide", _rc_disallowed_gpgx_wide_settings }, { "Mesen", _rc_disallowed_mesen_settings }, @@ -171,6 +196,7 @@ static const rc_disallowed_core_settings_t rc_disallowed_core_settings[] = { { "QUASI88", _rc_disallowed_quasi88_settings }, { "SMS Plus GX", _rc_disallowed_smsplus_settings }, { "Snes9x", _rc_disallowed_snes9x_settings }, + { "SwanStation", _rc_disallowed_swanstation_settings }, { "VICE x64", _rc_disallowed_vice_settings }, { "Virtual Jaguar", _rc_disallowed_virtual_jaguar_settings }, { NULL, NULL } @@ -186,6 +212,12 @@ static int rc_libretro_string_equal_nocase_wildcard(const char* test, const char return (*value == '\0'); } +static int rc_libretro_numeric_less_than(const char* test, const char* value) { + int test_num = atoi(test); + int value_num = atoi(value); + return (test_num < value_num); +} + static int rc_libretro_match_value(const char* val, const char* match) { /* if value starts with a comma, it's a CSV list of potential matches */ if (*match == ',') { @@ -218,6 +250,10 @@ static int rc_libretro_match_value(const char* val, const char* match) { 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); } diff --git a/3rdparty/rcheevos/src/rc_version.h b/3rdparty/rcheevos/src/rc_version.h index 7ed7301d27..4f8c25e364 100644 --- a/3rdparty/rcheevos/src/rc_version.h +++ b/3rdparty/rcheevos/src/rc_version.h @@ -8,7 +8,7 @@ RC_BEGIN_C_DECLS #define RCHEEVOS_VERSION_MAJOR 11 -#define RCHEEVOS_VERSION_MINOR 5 +#define RCHEEVOS_VERSION_MINOR 6 #define RCHEEVOS_VERSION_PATCH 0 #define RCHEEVOS_MAKE_VERSION(major, minor, patch) (major * 1000000 + minor * 1000 + patch) diff --git a/3rdparty/rcheevos/src/rcheevos/consoleinfo.c b/3rdparty/rcheevos/src/rcheevos/consoleinfo.c index b8bee226d4..98b43b6d04 100644 --- a/3rdparty/rcheevos/src/rcheevos/consoleinfo.c +++ b/3rdparty/rcheevos/src/rcheevos/consoleinfo.c @@ -721,16 +721,29 @@ static const rc_memory_regions_t rc_memory_regions_n64 = { _rc_memory_regions_n6 /* ===== Nintendo DS ===== */ /* https://www.akkit.org/info/gbatek.htm#dsmemorymaps */ static const rc_memory_region_t _rc_memory_regions_nintendo_ds[] = { - { 0x000000U, 0x3FFFFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } + { 0x0000000U, 0x03FFFFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + /* To keep DS/DSi memory maps aligned, padding is set here for the DSi's extra RAM */ + { 0x0400000U, 0x0FFFFFFU, 0x02400000U, RC_MEMORY_TYPE_UNUSED, "Unused (DSi exclusive)" }, + /* The DS/DSi have "tightly coupled memory": very fast memory directly connected to the CPU. + * This memory has an instruction variant (ITCM) and a data variant (DTCM). + * For achievement purposes it is useful to be able to access the data variant. + * This memory does not have a fixed address on console, being able to be moved to any $0xxxx000 region. + * While normally this kind of memory is addressed outside of the possible native addressing space, this is simply not possible, + * as the DS/DSi's address space covers all possible uint32_t values. + * $0E000000 is used here as a "pseudo-end," as this is nearly the end of all the memory actually mapped to addresses + * This means that (with the exception of $FFFF0000 onwards, which has the ARM9 BIOS mapped) $0E000000 onwards has nothing mapped to it + */ + { 0x1000000U, 0x1003FFFU, 0x0E000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Data TCM" } }; -static const rc_memory_regions_t rc_memory_regions_nintendo_ds = { _rc_memory_regions_nintendo_ds, 1 }; +static const rc_memory_regions_t rc_memory_regions_nintendo_ds = { _rc_memory_regions_nintendo_ds, 3 }; /* ===== Nintendo DSi ===== */ /* https://problemkaputt.de/gbatek.htm#dsiiomap */ static const rc_memory_region_t _rc_memory_regions_nintendo_dsi[] = { - { 0x000000U, 0xFFFFFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" } + { 0x0000000U, 0x0FFFFFFU, 0x02000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, + { 0x1000000U, 0x1003FFFU, 0x0E000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Data TCM" } }; -static const rc_memory_regions_t rc_memory_regions_nintendo_dsi = { _rc_memory_regions_nintendo_dsi, 1 }; +static const rc_memory_regions_t rc_memory_regions_nintendo_dsi = { _rc_memory_regions_nintendo_dsi, 2 }; /* ===== Oric ===== */ static const rc_memory_region_t _rc_memory_regions_oric[] = { @@ -956,6 +969,31 @@ static const rc_memory_region_t _rc_memory_regions_wonderswan[] = { }; static const rc_memory_regions_t rc_memory_regions_wonderswan = { _rc_memory_regions_wonderswan, 2 }; +/* ===== ZX Spectrum ===== */ +/* https://github.com/TASEmulators/BizHawk/blob/3a3b22c/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum16K/ZX16.cs + * https://github.com/TASEmulators/BizHawk/blob/3a3b22c/src/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Memory.cs + * https://worldofspectrum.org/faq/reference/128kreference.htm */ +static const rc_memory_region_t _rc_memory_regions_zx_spectrum[] = { + /* ZX Spectrum is complicated as multiple models exist with varying amounts of memory. + * In practice, this can be reduced to two categories: 16K/48K units, and 128K units. + * 16K/48K units have RAM starting at $4000 onwards, 16K ending at $7FFF, 48K ending at $FFFF. + * 128K units have banked memory, with $4000-$7FFF normally having RAM bank 5, and $8000-$BFFF normally having RAM bank 2. + * $C000-$FFFF is normally reserved for banked RAM, having any of banks 0-7. + * For the purposes of the RAM map, $C000-$FFFF is assumed to be bank 0, and $10000 onwards has the other banks in order (1, 3, 4, 6, 7) + * Doing it this way always for 16K/48K games to have the same memory map on the 128K, and thus avoid issues due to the model selected. + * Later 128K units also have a special banking mode that changes up banking completely, but for 16K/48K compatibility purposes this doesn't matter, and so is irrelevant. + */ + { 0x00000U, 0x03FFFU, 0x04000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Screen RAM" }, /* RAM bank 5 on 128K units */ + { 0x04000U, 0x07FFFU, 0x08000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 2 on 128K units */ + { 0x08000U, 0x0BFFFU, 0x0C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 0-7 on 128K units, assumed to be bank 0 here */ + { 0x0C000U, 0x0FFFFU, 0x10000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 1 on 128K units */ + { 0x10000U, 0x13FFFU, 0x14000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 3 on 128K units */ + { 0x14000U, 0x17FFFU, 0x18000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 4 on 128K units */ + { 0x18000U, 0x1BFFFU, 0x1C000U, RC_MEMORY_TYPE_SYSTEM_RAM, "System RAM" }, /* RAM bank 6 on 128K units */ + { 0x1C000U, 0x1FFFFU, 0x20000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Screen RAM" } /* RAM bank 7 on 128K units */ +}; +static const rc_memory_regions_t rc_memory_regions_zx_spectrum = { _rc_memory_regions_zx_spectrum, 8 }; + /* ===== default ===== */ static const rc_memory_regions_t rc_memory_regions_none = { 0, 0 }; @@ -1141,6 +1179,9 @@ const rc_memory_regions_t* rc_console_memory_regions(uint32_t console_id) case RC_CONSOLE_WONDERSWAN: return &rc_memory_regions_wonderswan; + case RC_CONSOLE_ZX_SPECTRUM: + return &rc_memory_regions_zx_spectrum; + default: return &rc_memory_regions_none; } diff --git a/3rdparty/rcheevos/src/rcheevos/lboard.c b/3rdparty/rcheevos/src/rcheevos/lboard.c index 98b4ec2f17..47e05f2738 100644 --- a/3rdparty/rcheevos/src/rcheevos/lboard.c +++ b/3rdparty/rcheevos/src/rcheevos/lboard.c @@ -199,19 +199,19 @@ int rc_evaluate_lboard(rc_lboard_t* self, int32_t* value, rc_peek_t peek, void* /* start and submit are both true in the same frame, just submit without announcing the leaderboard is available */ self->state = RC_LBOARD_STATE_TRIGGERED; } - else if (self->start.requirement == 0 && self->start.alternative == 0) { - /* start condition is empty - this leaderboard is submit-only with no measured progress */ + else if (!self->start.requirement && !self->start.alternative) { + /* start trigger is empty. assume the leaderboard is in development and ignore */ } else { /* start the leaderboard attempt */ self->state = RC_LBOARD_STATE_STARTED; - - /* reset any hit counts in the value */ - if (self->progress) - rc_reset_value(self->progress); - - rc_reset_value(&self->value); } + + /* reset any hit counts in the value */ + if (self->progress) + rc_reset_value(self->progress); + + rc_reset_value(&self->value); } break; diff --git a/3rdparty/rcheevos/src/rcheevos/rc_validate.c b/3rdparty/rcheevos/src/rcheevos/rc_validate.c index bd3b305a4b..770f9dc987 100644 --- a/3rdparty/rcheevos/src/rcheevos/rc_validate.c +++ b/3rdparty/rcheevos/src/rcheevos/rc_validate.c @@ -726,6 +726,7 @@ static int rc_validate_conflicting_conditions(const rc_condset_t* conditions, co const rc_condition_t* condition; const rc_condition_t* condition_chain_start; int overlap; + int chain_matches; /* empty group */ if (conditions == NULL || compare_conditions == NULL) @@ -777,9 +778,9 @@ static int rc_validate_conflicting_conditions(const rc_condset_t* conditions, co /* if combining conditions exist, make sure the same combining conditions exist in the * compare logic. conflicts can only occur if the combinining conditions match. */ + chain_matches = 1; if (condition_chain_start != condition) { - int chain_matches = 1; const rc_condition_t* condition_chain_iter = condition_chain_start; while (condition_chain_iter != condition) { @@ -795,11 +796,8 @@ static int rc_validate_conflicting_conditions(const rc_condset_t* conditions, co if (compare_condition->oper != RC_OPERATOR_NONE && !rc_validate_are_operands_equal(&compare_condition->operand2, &condition_chain_iter->operand2)) { - if (compare_condition->operand2.type != condition_chain_iter->operand2.type) - { - chain_matches = 0; - break; - } + chain_matches = 0; + break; } if (!compare_condition->next) @@ -808,17 +806,27 @@ static int rc_validate_conflicting_conditions(const rc_condset_t* conditions, co break; } + if (compare_condition->type != RC_CONDITION_ADD_ADDRESS && + compare_condition->type != RC_CONDITION_ADD_SOURCE && + compare_condition->type != RC_CONDITION_SUB_SOURCE && + compare_condition->type != RC_CONDITION_AND_NEXT) + { + /* things like AddHits and OrNext are hard to definitively detect conflicts. ignore them. */ + chain_matches = 0; + break; + } + compare_condition = compare_condition->next; condition_chain_iter = condition_chain_iter->next; } + } - /* combining field didn't match, or there's more unmatched combining fields. ignore this condition */ - if (!chain_matches || rc_validate_is_combining_condition(compare_condition)) - { - while (compare_condition->next && rc_validate_is_combining_condition(compare_condition)) - compare_condition = compare_condition->next; - continue; - } + /* combining field didn't match, or there's more unmatched combining fields. ignore this condition */ + if (!chain_matches || rc_validate_is_combining_condition(compare_condition)) + { + while (compare_condition->next && rc_validate_is_combining_condition(compare_condition)) + compare_condition = compare_condition->next; + continue; } if (compare_condition->required_hits) diff --git a/3rdparty/rcheevos/src/rcheevos/richpresence.c b/3rdparty/rcheevos/src/rcheevos/richpresence.c index 0dd660e686..81fc56a100 100644 --- a/3rdparty/rcheevos/src/rcheevos/richpresence.c +++ b/3rdparty/rcheevos/src/rcheevos/richpresence.c @@ -360,6 +360,9 @@ static const char* rc_parse_richpresence_lookup(rc_richpresence_lookup_t* lookup do { line = nextline; + if (line == NULL) + break; + nextline = rc_parse_line(line, &endline, parse); if (endline - line < 2) { @@ -438,6 +441,9 @@ static const char* rc_parse_richpresence_lookup(rc_richpresence_lookup_t* lookup /* insert the current item and continue scanning the next one */ rc_insert_richpresence_lookup_item(lookup, first, last, label, (int)(endline - label), parse); + if (parse->offset < 0) + break; + line = endptr + 1; } while (line < endline); diff --git a/3rdparty/rcheevos/src/rhash/hash.c b/3rdparty/rcheevos/src/rhash/hash.c index 9fc32f6324..d9ad0eb779 100644 --- a/3rdparty/rcheevos/src/rhash/hash.c +++ b/3rdparty/rcheevos/src/rhash/hash.c @@ -3113,6 +3113,7 @@ int rc_hash_generate_from_buffer(char hash[33], uint32_t console_id, const uint8 case RC_CONSOLE_VIRTUAL_BOY: case RC_CONSOLE_WASM4: case RC_CONSOLE_WONDERSWAN: + case RC_CONSOLE_ZX_SPECTRUM: return rc_hash_buffer(hash, buffer, buffer_size); case RC_CONSOLE_ARDUBOY: @@ -3411,6 +3412,7 @@ int rc_hash_generate_from_file(char hash[33], uint32_t console_id, const char* p case RC_CONSOLE_VIRTUAL_BOY: case RC_CONSOLE_WASM4: case RC_CONSOLE_WONDERSWAN: + case RC_CONSOLE_ZX_SPECTRUM: /* generic whole-file hash - don't buffer */ return rc_hash_whole_file(hash, path); @@ -3466,6 +3468,9 @@ int rc_hash_generate_from_file(char hash[33], uint32_t console_id, const char* p case RC_CONSOLE_NINTENDO_64: return rc_hash_n64(hash, path); + case RC_CONSOLE_NINTENDO_3DS: + return rc_hash_nintendo_3ds(hash, path); + case RC_CONSOLE_NINTENDO_DS: case RC_CONSOLE_NINTENDO_DSI: return rc_hash_nintendo_ds(hash, path); @@ -3573,6 +3578,7 @@ static void rc_hash_initialize_dsk_iterator(struct rc_hash_iterator* iterator, c /* check MSX first, as Apple II isn't supported by RetroArch, and RAppleWin won't use the iterator */ rc_hash_iterator_append_console(iterator, RC_CONSOLE_MSX); rc_hash_iterator_append_console(iterator, RC_CONSOLE_AMSTRAD_PC); + rc_hash_iterator_append_console(iterator, RC_CONSOLE_ZX_SPECTRUM); rc_hash_iterator_append_console(iterator, RC_CONSOLE_APPLE_II); } @@ -3725,6 +3731,10 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* { 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': @@ -3808,6 +3818,7 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* 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; @@ -3909,6 +3920,10 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* { 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': @@ -3947,11 +3962,16 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* { 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")) @@ -3962,6 +3982,11 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char* { 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':