From 94657ae4ab890775b955095eae6d36c530505226 Mon Sep 17 00:00:00 2001
From: Stenzek <stenzek@gmail.com>
Date: Sat, 24 Feb 2024 14:52:57 +1000
Subject: [PATCH] dep/rcheevos: Update to 3d01191

---
 dep/rcheevos/CMakeLists.txt                   |   2 +-
 dep/rcheevos/include/rc_api_request.h         |   2 +-
 dep/rcheevos/include/rc_client.h              |  19 +
 .../include/rc_client_raintegration.h         |   4 +
 dep/rcheevos/{src => include}/rc_util.h       |   0
 dep/rcheevos/rcheevos.vcxproj                 |   2 +-
 dep/rcheevos/rcheevos.vcxproj.filters         |   4 +-
 dep/rcheevos/src/rapi/rc_api_common.c         |   3 +
 dep/rcheevos/src/rc_client.c                  | 121 +++++--
 dep/rcheevos/src/rc_client_external.h         |   1 +
 dep/rcheevos/src/rc_client_internal.h         |  15 +-
 dep/rcheevos/src/rc_client_raintegration.c    |  23 +-
 .../src/rc_client_raintegration_internal.h    |   4 +
 dep/rcheevos/src/rc_compat.c                  |  27 +-
 dep/rcheevos/src/rc_libretro.c                |  88 ++---
 dep/rcheevos/src/rc_version.h                 |   2 +-
 dep/rcheevos/src/rcheevos/consoleinfo.c       |  26 ++
 dep/rcheevos/src/rcheevos/rc_internal.h       |   2 +-
 dep/rcheevos/src/rcheevos/runtime_progress.c  |   2 +-
 dep/rcheevos/src/rhash/hash.c                 | 328 +++++++++++++++++-
 20 files changed, 586 insertions(+), 89 deletions(-)
 rename dep/rcheevos/{src => include}/rc_util.h (100%)

diff --git a/dep/rcheevos/CMakeLists.txt b/dep/rcheevos/CMakeLists.txt
index e17d4c74e..ddcb543ce 100644
--- a/dep/rcheevos/CMakeLists.txt
+++ b/dep/rcheevos/CMakeLists.txt
@@ -12,6 +12,7 @@ add_library(rcheevos
   include/rc_runtime.h
   include/rc_runtime_types.h
   include/rc_url.h
+  include/rc_util.h
   src/rapi/rc_api_common.c
   src/rapi/rc_api_common.h
   src/rapi/rc_api_editor.c
@@ -39,7 +40,6 @@ add_library(rcheevos
   src/rc_compat.c
   src/rc_compat.h
   src/rc_util.c
-  src/rc_util.h
   src/rc_version.h
   src/rhash/cdreader.c
   src/rhash/hash.c
diff --git a/dep/rcheevos/include/rc_api_request.h b/dep/rcheevos/include/rc_api_request.h
index 4eba7f0d7..dd72fb56d 100644
--- a/dep/rcheevos/include/rc_api_request.h
+++ b/dep/rcheevos/include/rc_api_request.h
@@ -2,7 +2,7 @@
 #define RC_API_REQUEST_H
 
 #include "rc_error.h"
-#include "../src/rc_util.h"
+#include "rc_util.h"
 
 #include <stddef.h>
 
diff --git a/dep/rcheevos/include/rc_client.h b/dep/rcheevos/include/rc_client.h
index 5659aa74f..9631b52dd 100644
--- a/dep/rcheevos/include/rc_client.h
+++ b/dep/rcheevos/include/rc_client.h
@@ -136,6 +136,11 @@ RC_EXPORT void RC_CCONV rc_client_set_get_time_millisecs_function(rc_client_t* c
  */
 RC_EXPORT void RC_CCONV rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle);
 
+/**
+ * Gets a clause that can be added to the User-Agent to identify the version of rcheevos being used.
+ */
+RC_EXPORT size_t RC_CCONV rc_client_get_user_agent_clause(rc_client_t* client, char buffer[], size_t buffer_size);
+
 /*****************************************************************************\
 | Logging                                                                     |
 \*****************************************************************************/
@@ -230,6 +235,20 @@ RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_identify_and_load_g
 RC_EXPORT rc_client_async_handle_t* RC_CCONV rc_client_begin_load_game(rc_client_t* client, const char* hash,
     rc_client_callback_t callback, void* callback_userdata);
 
+/**
+ * Gets the current progress of the asynchronous load game process.
+ */
+RC_EXPORT int RC_CCONV rc_client_get_load_game_state(const rc_client_t* client);
+enum {
+  RC_CLIENT_LOAD_GAME_STATE_NONE,
+  RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME,
+  RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN,
+  RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA,
+  RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION,
+  RC_CLIENT_LOAD_GAME_STATE_DONE,
+  RC_CLIENT_LOAD_GAME_STATE_ABORTED
+};
+
 /**
  * Unloads the current game.
  */
diff --git a/dep/rcheevos/include/rc_client_raintegration.h b/dep/rcheevos/include/rc_client_raintegration.h
index b2e77cdec..461129104 100644
--- a/dep/rcheevos/include/rc_client_raintegration.h
+++ b/dep/rcheevos/include/rc_client_raintegration.h
@@ -46,6 +46,8 @@ typedef void (RC_CCONV *rc_client_raintegration_event_handler_t)(const rc_client
 typedef void (RC_CCONV *rc_client_raintegration_write_memory_func_t)(uint32_t address, uint8_t* buffer,
                                                                      uint32_t num_bytes, rc_client_t* client);
 
+typedef void (RC_CCONV* rc_client_raintegration_get_game_name_func_t)(char* buffer, uint32_t buffer_size, rc_client_t* client);
+
 /* types needed to integrate raintegration */
 
 #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
@@ -74,6 +76,8 @@ RC_EXPORT void RC_CCONV rc_client_raintegration_update_menu_item(const rc_client
 RC_EXPORT int RC_CCONV rc_client_raintegration_activate_menu_item(const rc_client_t* client, uint32_t nMenuItemId);
 
 RC_EXPORT void RC_CCONV rc_client_raintegration_set_write_memory_function(rc_client_t* client, rc_client_raintegration_write_memory_func_t handler);
+RC_EXPORT void RC_CCONV rc_client_raintegration_set_get_game_name_function(rc_client_t* client, rc_client_raintegration_get_game_name_func_t handler);
+RC_EXPORT int RC_CCONV rc_client_raintegration_has_modifications(const rc_client_t* client);
 
 RC_EXPORT void RC_CCONV rc_client_raintegration_set_event_handler(rc_client_t* client,
     rc_client_raintegration_event_handler_t handler);
diff --git a/dep/rcheevos/src/rc_util.h b/dep/rcheevos/include/rc_util.h
similarity index 100%
rename from dep/rcheevos/src/rc_util.h
rename to dep/rcheevos/include/rc_util.h
diff --git a/dep/rcheevos/rcheevos.vcxproj b/dep/rcheevos/rcheevos.vcxproj
index 115f00d7f..774ff5d0b 100644
--- a/dep/rcheevos/rcheevos.vcxproj
+++ b/dep/rcheevos/rcheevos.vcxproj
@@ -43,12 +43,12 @@
     <ClInclude Include="include\rc_runtime.h" />
     <ClInclude Include="include\rc_runtime_types.h" />
     <ClInclude Include="include\rc_url.h" />
+    <ClInclude Include="include\rc_util.h" />
     <ClInclude Include="src\rapi\rc_api_common.h" />
     <ClInclude Include="src\rcheevos\rc_internal.h" />
     <ClInclude Include="src\rcheevos\rc_validate.h" />
     <ClInclude Include="src\rc_client_internal.h" />
     <ClInclude Include="src\rc_compat.h" />
-    <ClInclude Include="src\rc_util.h" />
     <ClInclude Include="src\rc_version.h" />
     <ClInclude Include="src\rhash\md5.h" />
   </ItemGroup>
diff --git a/dep/rcheevos/rcheevos.vcxproj.filters b/dep/rcheevos/rcheevos.vcxproj.filters
index 2baa43b20..d262e0bee 100644
--- a/dep/rcheevos/rcheevos.vcxproj.filters
+++ b/dep/rcheevos/rcheevos.vcxproj.filters
@@ -144,8 +144,10 @@
       <Filter>rcheevos</Filter>
     </ClInclude>
     <ClInclude Include="src\rc_compat.h" />
-    <ClInclude Include="src\rc_util.h" />
     <ClInclude Include="src\rc_version.h" />
     <ClInclude Include="src\rc_client_internal.h" />
+    <ClInclude Include="include\rc_util.h">
+      <Filter>include</Filter>
+    </ClInclude>
   </ItemGroup>
 </Project>
\ No newline at end of file
diff --git a/dep/rcheevos/src/rapi/rc_api_common.c b/dep/rcheevos/src/rapi/rc_api_common.c
index 5835b2801..407efe55f 100644
--- a/dep/rcheevos/src/rapi/rc_api_common.c
+++ b/dep/rcheevos/src/rapi/rc_api_common.c
@@ -1150,6 +1150,9 @@ static void rc_api_update_host(char** host, const char* hostname) {
 }
 
 void rc_api_set_host(const char* hostname) {
+  if (hostname && strcmp(hostname, RETROACHIEVEMENTS_HOST) == 0)
+    hostname = NULL;
+
   rc_api_update_host(&g_host, hostname);
 
   if (!hostname) {
diff --git a/dep/rcheevos/src/rc_client.c b/dep/rcheevos/src/rc_client.c
index 0028f4132..fe593392c 100644
--- a/dep/rcheevos/src/rc_client.c
+++ b/dep/rcheevos/src/rc_client.c
@@ -5,6 +5,7 @@
 #include "rc_api_user.h"
 #include "rc_consoles.h"
 #include "rc_hash.h"
+#include "rc_version.h"
 
 #include "rapi/rc_api_common.h"
 
@@ -611,7 +612,7 @@ static void rc_client_login_callback(const rc_api_server_response_t* server_resp
     if (login_callback_data->callback)
       login_callback_data->callback(result, error_message, client, login_callback_data->callback_userdata);
 
-    if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN)
+    if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN)
       rc_client_begin_fetch_game_data(load_state);
   }
   else {
@@ -634,7 +635,7 @@ static void rc_client_login_callback(const rc_api_server_response_t* server_resp
 
     RC_CLIENT_LOG_INFO_FORMATTED(client, "%s logged in successfully", login_response.display_name);
 
-    if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN)
+    if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN)
       rc_client_begin_fetch_game_data(load_state);
 
     if (login_callback_data->callback)
@@ -795,7 +796,7 @@ void rc_client_logout(rc_client_t* client)
 
   rc_client_unload_game(client);
 
-  if (load_state && load_state->progress == RC_CLIENT_LOAD_STATE_AWAIT_LOGIN)
+  if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN)
     rc_client_load_error(load_state, RC_ABORTED, "Login aborted");
 }
 
@@ -936,9 +937,9 @@ static int rc_client_end_load_state(rc_client_load_state_t* load_state)
      * the outstanding_requests count will reach zero and the memory will be free'd then. */
     if (remaining_requests == 0) {
       /* if one of the callbacks called rc_client_load_error, progress will be set to
-       * RC_CLIENT_LOAD_STATE_UNKNOWN. There's no need to call the callback with RC_ABORTED
+       * RC_CLIENT_LOAD_STATE_ABORTED. There's no need to call the callback with RC_ABORTED
        * in that case, as it will have already been called with something more appropriate. */
-      if (load_state->progress != RC_CLIENT_LOAD_STATE_UNKNOWN_GAME && load_state->callback)
+      if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_ABORTED && load_state->callback)
         load_state->callback(RC_ABORTED, "The requested game is no longer active", load_state->client, load_state->callback_userdata);
 
       rc_client_free_load_state(load_state);
@@ -956,7 +957,7 @@ static void rc_client_load_error(rc_client_load_state_t* load_state, int result,
 
   rc_mutex_lock(&load_state->client->state.mutex);
 
-  load_state->progress = RC_CLIENT_LOAD_STATE_UNKNOWN_GAME;
+  load_state->progress = RC_CLIENT_LOAD_GAME_STATE_ABORTED;
   if (load_state->client->state.load == load_state)
     load_state->client->state.load = NULL;
 
@@ -1020,6 +1021,8 @@ static void rc_client_invalidate_memref_leaderboards(rc_client_game_info_t* game
     for (; leaderboard < stop; ++leaderboard) {
       if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED)
         continue;
+      if (!leaderboard->lboard)
+        continue;
 
       if (rc_trigger_contains_memref(&leaderboard->lboard->start, memref))
         leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED;
@@ -1032,8 +1035,7 @@ static void rc_client_invalidate_memref_leaderboards(rc_client_game_info_t* game
       else
         continue;
 
-      if (leaderboard->lboard)
-        leaderboard->lboard->state = RC_LBOARD_STATE_DISABLED;
+      leaderboard->lboard->state = RC_LBOARD_STATE_DISABLED;
 
       RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled leaderboard %u. Invalid address %06X", leaderboard->public_.id, memref->address);
     }
@@ -1301,6 +1303,8 @@ static void rc_client_activate_leaderboards(rc_client_game_info_t* game, rc_clie
 {
   rc_client_leaderboard_info_t* leaderboard;
   rc_client_leaderboard_info_t* stop;
+  const uint8_t leaderboards_allowed =
+      client->state.hardcore || client->state.allow_leaderboards_in_softcore;
 
   uint32_t active_count = 0;
   rc_client_subset_info_t* subset = game->subsets;
@@ -1317,7 +1321,7 @@ static void rc_client_activate_leaderboards(rc_client_game_info_t* game, rc_clie
           continue;
 
         case RC_CLIENT_LEADERBOARD_STATE_INACTIVE:
-          if (client->state.hardcore) {
+          if (leaderboards_allowed) {
             rc_reset_lboard(leaderboard->lboard);
             leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE;
             ++active_count;
@@ -1325,7 +1329,7 @@ static void rc_client_activate_leaderboards(rc_client_game_info_t* game, rc_clie
           break;
 
         default:
-          if (client->state.hardcore)
+          if (leaderboards_allowed)
             ++active_count;
           else
             leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE;
@@ -1403,11 +1407,11 @@ 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_STATE_DONE : RC_CLIENT_LOAD_STATE_UNKNOWN_GAME;
+      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_STATE_DONE) {
+  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);
@@ -1557,7 +1561,7 @@ static void rc_client_begin_start_session(rc_client_load_state_t* load_state)
     rc_client_load_error(load_state, result, rc_error_str(result));
   }
   else {
-    rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_STARTING_SESSION, 1);
+    rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION, 1);
     RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Starting session for game %u", start_session_params.game_id);
     rc_client_begin_async(client, &load_state->async_handle);
     client->callbacks.server_call(&start_session_request, rc_client_start_session_callback, load_state, client);
@@ -1872,7 +1876,7 @@ static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* s
     }
 
     /* kick off the start session request while we process the game data */
-    rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_STARTING_SESSION, 1);
+    rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION, 1);
     if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) {
       /* we can't unlock achievements without a session, lock spectator mode for the game */
       load_state->client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_LOCKED;
@@ -2006,18 +2010,30 @@ static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state)
       /* only a single hash was tried, capture it */
       load_state->game->public_.console_id = load_state->hash_console_id;
       load_state->game->public_.hash = load_state->hash->hash;
+
+      if (client->callbacks.identify_unknown_hash) {
+        load_state->hash->game_id = client->callbacks.identify_unknown_hash(
+            load_state->hash_console_id, load_state->hash->hash, client, load_state->callback_userdata);
+
+        if (load_state->hash->game_id != 0) {
+          RC_CLIENT_LOG_INFO_FORMATTED(load_state->client, "Client says to load game %u for unidentified hash %s",
+            load_state->hash->game_id, load_state->hash->hash);
+        }
+      }
     }
 
-    load_state->game->public_.title = "Unknown Game";
-    load_state->game->public_.badge_name = "";
-    client->game = load_state->game;
-    load_state->game = NULL;
+    if (load_state->hash->game_id == 0) {
+      load_state->game->public_.title = "Unknown Game";
+      load_state->game->public_.badge_name = "";
+      client->game = load_state->game;
+      load_state->game = NULL;
 
-    rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game");
-    return;
+      rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game");
+      return;
+    }
   }
 
-  if (load_state->hash->hash[0] != '[') {
+  if (load_state->hash->hash[0] != '[') { /* not [NO HASH] or [SUBSETxx] */
     load_state->game->public_.id = load_state->hash->game_id;
     load_state->game->public_.hash = load_state->hash->hash;
   }
@@ -2028,7 +2044,7 @@ static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state)
   rc_mutex_lock(&client->state.mutex);
   result = client->state.user;
   if (result == RC_CLIENT_USER_STATE_LOGIN_REQUESTED)
-    load_state->progress = RC_CLIENT_LOAD_STATE_AWAIT_LOGIN;
+    load_state->progress = RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN;
   rc_mutex_unlock(&client->state.mutex);
 
   switch (result) {
@@ -2055,7 +2071,7 @@ static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state)
     return;
   }
 
-  rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_FETCHING_GAME_DATA, 1);
+  rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA, 1);
 
   RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Fetching data for game %u", fetch_game_data_request.game_id);
   rc_client_begin_async(client, &load_state->async_handle);
@@ -2197,7 +2213,7 @@ static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* loa
       return NULL;
     }
 
-    rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_IDENTIFYING_GAME, 1);
+    rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME, 1);
 
     rc_client_begin_async(client, &load_state->async_handle);
     client->callbacks.server_call(&request, rc_client_identify_game_callback, load_state, client);
@@ -2336,6 +2352,20 @@ rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* cl
   return rc_client_load_game(load_state, hash, file_path);
 }
 
+int rc_client_get_load_game_state(const rc_client_t* client)
+{
+  int state = RC_CLIENT_LOAD_GAME_STATE_NONE;
+  if (client) {
+    const rc_client_load_state_t* load_state = client->state.load;
+    if (load_state)
+      state = load_state->progress;
+    else if (client->game)
+      state = RC_CLIENT_LOAD_GAME_STATE_DONE;
+  }
+
+  return state;
+}
+
 static void rc_client_game_mark_ui_to_be_hidden(rc_client_t* client, rc_client_game_info_t* game)
 {
   rc_client_achievement_info_t* achievement;
@@ -3749,7 +3779,7 @@ int rc_client_has_leaderboards(rc_client_t* client)
   return result;
 }
 
-static void rc_client_allocate_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard)
+void rc_client_allocate_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard)
 {
   rc_client_leaderboard_tracker_info_t* tracker;
   rc_client_leaderboard_tracker_info_t* available_tracker = NULL;
@@ -4007,6 +4037,11 @@ static void rc_client_submit_leaderboard_entry(rc_client_t* client, rc_client_le
 {
   rc_client_submit_leaderboard_entry_callback_data_t* callback_data;
 
+  if (!client->state.hardcore) {
+    RC_CLIENT_LOG_INFO_FORMATTED(client, "Leaderboard %u entry submission not allowed in softcore", leaderboard->public_.id);
+    return;
+  }
+
   if (client->callbacks.can_submit_leaderboard_entry &&
       !client->callbacks.can_submit_leaderboard_entry(leaderboard->public_.id, client)) {
     RC_CLIENT_LOG_INFO_FORMATTED(client, "Leaderboard %u entry submission blocked by client", leaderboard->public_.id);
@@ -4545,6 +4580,11 @@ static void rc_client_do_frame_process_achievements(rc_client_t* client, rc_clie
     old_state = trigger->state;
     new_state = rc_evaluate_trigger(trigger, client->state.legacy_peek, client, NULL);
 
+    /* trigger->state doesn't actually change to RESET - RESET just serves as a notification.
+     * we don't care about that particular notification, so look at the actual state. */
+    if (new_state == RC_TRIGGER_STATE_RESET)
+      new_state = trigger->state;
+
     /* if the measured value changed and the achievement hasn't triggered, show a progress indicator */
     if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN &&
         trigger->measured_value <= trigger->measured_target &&
@@ -4940,7 +4980,7 @@ void rc_client_do_frame(rc_client_t* client)
     if (client->game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER)
       rc_client_do_frame_update_progress_tracker(client, client->game);
 
-    if (client->state.hardcore) {
+    if (client->state.hardcore || client->state.allow_leaderboards_in_softcore) {
       for (subset = client->game->subsets; subset; subset = subset->next) {
         if (subset->active)
           rc_client_do_frame_process_leaderboards(client, subset);
@@ -5406,7 +5446,9 @@ static void rc_client_disable_hardcore(rc_client_t* client)
 
   if (client->game) {
     rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE);
-    rc_client_deactivate_leaderboards(client->game, client);
+
+    if (!client->state.allow_leaderboards_in_softcore)
+      rc_client_deactivate_leaderboards(client->game, client);
   }
 }
 
@@ -5592,3 +5634,28 @@ void rc_client_set_host(const rc_client_t* client, const char* hostname)
     client->state.external_client->set_host(hostname);
 #endif
 }
+
+size_t rc_client_get_user_agent_clause(rc_client_t* client, char buffer[], size_t buffer_size)
+{
+  size_t result;
+
+#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
+  if (client && client->state.external_client && client->state.external_client->get_user_agent_clause) {
+    result = client->state.external_client->get_user_agent_clause(buffer, buffer_size);
+    if (result > 0) {
+      result += snprintf(buffer + result, buffer_size - result, " rc_client/" RCHEEVOS_VERSION_STRING);
+      buffer[buffer_size - 1] = '\0';
+      return result;
+    }
+  }
+#else
+  (void)client;
+#endif
+
+  result = snprintf(buffer, buffer_size, "rcheevos/" RCHEEVOS_VERSION_STRING);
+
+  /* some implementations of snprintf will fill the buffer without null terminating.
+   * make sure the buffer is null terminated */
+  buffer[buffer_size - 1] = '\0';
+  return result;
+}
diff --git a/dep/rcheevos/src/rc_client_external.h b/dep/rcheevos/src/rc_client_external.h
index 1d625421e..a519e428e 100644
--- a/dep/rcheevos/src/rc_client_external.h
+++ b/dep/rcheevos/src/rc_client_external.h
@@ -73,6 +73,7 @@ typedef struct rc_client_external_t
   rc_client_external_set_read_memory_func_t set_read_memory;
   rc_client_external_set_get_time_millisecs_func_t set_get_time_millisecs;
   rc_client_external_set_string_func_t set_host;
+  rc_client_external_copy_string_func_t get_user_agent_clause;
 
   rc_client_external_set_int_func_t set_hardcore_enabled;
   rc_client_external_get_int_func_t get_hardcore_enabled;
diff --git a/dep/rcheevos/src/rc_client_internal.h b/dep/rcheevos/src/rc_client_internal.h
index 509c56750..de8258630 100644
--- a/dep/rcheevos/src/rc_client_internal.h
+++ b/dep/rcheevos/src/rc_client_internal.h
@@ -26,6 +26,8 @@ typedef void (RC_CCONV *rc_client_post_process_game_data_response_t)(const rc_ap
 typedef int (RC_CCONV *rc_client_can_submit_achievement_unlock_t)(uint32_t achievement_id, rc_client_t* client);
 typedef int (RC_CCONV *rc_client_can_submit_leaderboard_entry_t)(uint32_t leaderboard_id, rc_client_t* client);
 typedef int (RC_CCONV *rc_client_rich_presence_override_t)(rc_client_t* client, char buffer[], size_t buffersize);
+typedef uint32_t (RC_CCONV* rc_client_identify_hash_func_t)(uint32_t console_id, const char* hash,
+                  rc_client_t* client, void* callback_userdata);
 
 typedef struct rc_client_callbacks_t {
   rc_client_read_memory_func_t read_memory;
@@ -33,6 +35,7 @@ typedef struct rc_client_callbacks_t {
   rc_client_server_call_t server_call;
   rc_client_message_callback_t log_call;
   rc_get_time_millisecs_func_t get_time_millisecs;
+  rc_client_identify_hash_func_t identify_unknown_hash;
   rc_client_post_process_game_data_response_t post_process_game_data_response;
   rc_client_can_submit_achievement_unlock_t can_submit_achievement_unlock;
   rc_client_can_submit_leaderboard_entry_t can_submit_leaderboard_entry;
@@ -265,16 +268,6 @@ void rc_client_update_active_leaderboards(rc_client_game_info_t* game);
 | Client                                                                      |
 \*****************************************************************************/
 
-enum {
-  RC_CLIENT_LOAD_STATE_NONE,
-  RC_CLIENT_LOAD_STATE_IDENTIFYING_GAME,
-  RC_CLIENT_LOAD_STATE_AWAIT_LOGIN,
-  RC_CLIENT_LOAD_STATE_FETCHING_GAME_DATA,
-  RC_CLIENT_LOAD_STATE_STARTING_SESSION,
-  RC_CLIENT_LOAD_STATE_DONE,
-  RC_CLIENT_LOAD_STATE_UNKNOWN_GAME
-};
-
 enum {
   RC_CLIENT_USER_STATE_NONE,
   RC_CLIENT_USER_STATE_LOGIN_REQUESTED,
@@ -325,6 +318,7 @@ typedef struct rc_client_state_t {
   uint8_t log_level;
   uint8_t user;
   uint8_t disconnect;
+  uint8_t allow_leaderboards_in_softcore;
 
   struct rc_client_load_state_t* load;
   struct rc_client_async_handle_t* async_handles[4];
@@ -386,6 +380,7 @@ enum {
 
 void rc_client_set_legacy_peek(rc_client_t* client, int method);
 
+void rc_client_allocate_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard);
 void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard);
 
 RC_END_C_DECLS
diff --git a/dep/rcheevos/src/rc_client_raintegration.c b/dep/rcheevos/src/rc_client_raintegration.c
index efea7e449..227d03a84 100644
--- a/dep/rcheevos/src/rc_client_raintegration.c
+++ b/dep/rcheevos/src/rc_client_raintegration.c
@@ -77,7 +77,9 @@ static void rc_client_raintegration_load_dll(rc_client_t* client,
   raintegration->get_menu = (rc_client_raintegration_get_menu_func_t)GetProcAddress(hDLL, "_Rcheevos_RAIntegrationGetMenu");
   raintegration->activate_menu_item = (rc_client_raintegration_activate_menuitem_func_t)GetProcAddress(hDLL, "_Rcheevos_ActivateRAIntegrationMenuItem");
   raintegration->set_write_memory_function = (rc_client_raintegration_set_write_memory_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationWriteMemoryFunction");
+  raintegration->set_get_game_name_function = (rc_client_raintegration_set_get_game_name_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationGetGameNameFunction");
   raintegration->set_event_handler = (rc_client_raintegration_set_event_handler_func_t)GetProcAddress(hDLL, "_Rcheevos_SetRAIntegrationEventHandler");
+  raintegration->has_modifications = (rc_client_raintegration_get_int_func_t)GetProcAddress(hDLL, "_Rcheevos_HasModifications");
 
   if (!raintegration->get_version ||
       !raintegration->init_client ||
@@ -147,7 +149,8 @@ static void rc_client_init_raintegration(rc_client_t* client,
     const char* host_url = client->state.raintegration->get_host_url();
     if (host_url) {
       if (strcmp(host_url, "OFFLINE") != 0) {
-        rc_client_set_host(client, host_url);
+        if (strcmp(host_url, "https://retroachievements.org") != 0)
+          rc_client_set_host(client, host_url);
       }
       else if (client->state.raintegration->init_client_offline) {
         init_func = client->state.raintegration->init_client_offline;
@@ -363,6 +366,12 @@ void rc_client_raintegration_set_write_memory_function(rc_client_t* client, rc_c
     client->state.raintegration->set_write_memory_function(client, handler);
 }
 
+void rc_client_raintegration_set_get_game_name_function(rc_client_t* client, rc_client_raintegration_get_game_name_func_t handler)
+{
+  if (client && client->state.raintegration && client->state.raintegration->set_get_game_name_function)
+    client->state.raintegration->set_get_game_name_function(client, handler);
+}
+
 void rc_client_raintegration_set_event_handler(rc_client_t* client,
     rc_client_raintegration_event_handler_t handler)
 {
@@ -382,6 +391,18 @@ const rc_client_raintegration_menu_t* rc_client_raintegration_get_menu(const rc_
   return client->state.raintegration->get_menu();
 }
 
+int rc_client_raintegration_has_modifications(const rc_client_t* client)
+{
+  if (!client || !client->state.raintegration ||
+    !client->state.raintegration->bIsInited ||
+    !client->state.raintegration->has_modifications)
+  {
+    return 0;
+  }
+
+  return client->state.raintegration->has_modifications();
+}
+
 void rc_client_raintegration_rebuild_submenu(rc_client_t* client, HMENU hMenu)
 {
    HMENU hPopupMenu = NULL;
diff --git a/dep/rcheevos/src/rc_client_raintegration_internal.h b/dep/rcheevos/src/rc_client_raintegration_internal.h
index 530d98e1a..ce7c98b03 100644
--- a/dep/rcheevos/src/rc_client_raintegration_internal.h
+++ b/dep/rcheevos/src/rc_client_raintegration_internal.h
@@ -20,7 +20,9 @@ typedef void (RC_CCONV* rc_client_raintegration_hwnd_action_func_t)(HWND hWnd);
 typedef const rc_client_raintegration_menu_t* (RC_CCONV* rc_client_raintegration_get_menu_func_t)(void);
 typedef int (RC_CCONV* rc_client_raintegration_activate_menuitem_func_t)(uint32_t nMenuItemId);
 typedef void (RC_CCONV* rc_client_raintegration_set_write_memory_func_t)(rc_client_t* pClient, rc_client_raintegration_write_memory_func_t handler);
+typedef void (RC_CCONV* rc_client_raintegration_set_get_game_name_func_t)(rc_client_t* pClient, rc_client_raintegration_get_game_name_func_t handler);
 typedef void (RC_CCONV* rc_client_raintegration_set_event_handler_func_t)(rc_client_t* pClient, rc_client_raintegration_event_handler_t handler);
+typedef int (RC_CCONV* rc_client_raintegration_get_int_func_t)(void);
 
 typedef struct rc_client_raintegration_t
 {
@@ -37,9 +39,11 @@ typedef struct rc_client_raintegration_t
   rc_client_raintegration_hwnd_action_func_t update_main_window_handle;
 
   rc_client_raintegration_set_write_memory_func_t set_write_memory_function;
+  rc_client_raintegration_set_get_game_name_func_t set_get_game_name_function;
   rc_client_raintegration_set_event_handler_func_t set_event_handler;
   rc_client_raintegration_get_menu_func_t get_menu;
   rc_client_raintegration_activate_menuitem_func_t activate_menu_item;
+  rc_client_raintegration_get_int_func_t has_modifications;
 
   rc_client_raintegration_get_external_client_func_t get_external_client;
 
diff --git a/dep/rcheevos/src/rc_compat.c b/dep/rcheevos/src/rc_compat.c
index 0ba7601ad..6a8a5de57 100644
--- a/dep/rcheevos/src/rc_compat.c
+++ b/dep/rcheevos/src/rc_compat.c
@@ -85,7 +85,8 @@ struct tm* rc_gmtime_s(struct tm* buf, const time_t* timer)
 #endif
 
 #ifndef RC_NO_THREADS
-#ifdef _WIN32
+
+#if defined(_WIN32)
 
 /* https://gist.github.com/roxlu/1c1af99f92bafff9d8d9 */
 
@@ -113,6 +114,30 @@ void rc_mutex_unlock(rc_mutex_t* mutex)
   ReleaseMutex(mutex->handle);
 }
 
+#elif defined(GEKKO)
+
+/* https://github.com/libretro/RetroArch/pull/16116 */
+
+void rc_mutex_init(rc_mutex_t* mutex)
+{
+  LWP_MutexInit(mutex, NULL);
+}
+
+void rc_mutex_destroy(rc_mutex_t* mutex)
+{
+  LWP_MutexDestroy(mutex);
+}
+
+void rc_mutex_lock(rc_mutex_t* mutex)
+{
+  LWP_MutexLock(mutex);
+}
+
+void rc_mutex_unlock(rc_mutex_t* mutex)
+{
+  LWP_MutexUnlock(mutex);
+}
+
 #else
 
 void rc_mutex_init(rc_mutex_t* mutex)
diff --git a/dep/rcheevos/src/rc_libretro.c b/dep/rcheevos/src/rc_libretro.c
index 70bf06be1..d94d6d5b4 100644
--- a/dep/rcheevos/src/rc_libretro.c
+++ b/dep/rcheevos/src/rc_libretro.c
@@ -51,6 +51,11 @@ static const rc_disallowed_setting_t _rc_disallowed_dolphin_settings[] = {
   { NULL, NULL }
 };
 
+static const rc_disallowed_setting_t _rc_disallowed_dosbox_pure_settings[] = {
+  { "dosbox_pure_strict_mode", "false" },
+  { NULL, NULL }
+};
+
 static const rc_disallowed_setting_t _rc_disallowed_duckstation_settings[] = {
   { "duckstation_CDROM.LoadImagePatches", "true" },
   { NULL, NULL }
@@ -149,6 +154,7 @@ static const rc_disallowed_core_settings_t rc_disallowed_core_settings[] = {
   { "bsnes-mercury", _rc_disallowed_bsnes_settings },
   { "cap32", _rc_disallowed_cap32_settings },
   { "dolphin-emu", _rc_disallowed_dolphin_settings },
+  { "DOSBox-pure", _rc_disallowed_dosbox_pure_settings },
   { "DuckStation", _rc_disallowed_duckstation_settings },
   { "ecwolf", _rc_disallowed_ecwolf_settings },
   { "FCEUmm", _rc_disallowed_fceumm_settings },
@@ -324,27 +330,38 @@ uint8_t* rc_libretro_memory_find(const rc_libretro_memory_regions_t* regions, ui
 
 uint32_t rc_libretro_memory_read(const rc_libretro_memory_regions_t* regions, uint32_t address,
       uint8_t* buffer, uint32_t num_bytes) {
-  uint32_t i;
+  uint32_t bytes_read = 0;
   uint32_t avail;
+  uint32_t i;
 
   for (i = 0; i < regions->count; ++i) {
     const size_t size = regions->size[i];
-    if (address < size) {
-      if (regions->data[i] == NULL)
-        break;
-
-      avail = (unsigned)(size - address);
-      if (avail < num_bytes)
-         return avail;
-
-      memcpy(buffer, &regions->data[i][address], num_bytes);
-      return num_bytes;
+    if (address >= size) {
+      /* address is not in this block, adjust and look at next block */
+      address -= (unsigned)size;
+      continue;
     }
 
-    address -= (unsigned)size;
+    if (regions->data[i] == NULL) /* no memory associated to this block. abort */
+      break;
+
+    avail = (unsigned)(size - address);
+    if (avail >= num_bytes) {
+      /* requested memory is fully within this block, copy and return it */
+      memcpy(buffer, &regions->data[i][address], num_bytes);
+      bytes_read += num_bytes;
+      return bytes_read;
+    }
+
+    /* copy whatever is available in this block, and adjust for the next block */
+    memcpy(buffer, &regions->data[i][address], avail);
+    buffer += avail;
+    bytes_read += avail;
+    num_bytes -= avail;
+    address = 0;
   }
 
-  return 0;
+  return bytes_read;
 }
 
 void rc_libretro_init_verbose_message_callback(rc_libretro_message_callback callback) {
@@ -671,8 +688,7 @@ void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set,
     return;
 
   file_handle = rc_file_open(m3u_path);
-  if (!file_handle)
-  {
+  if (!file_handle) {
     rc_hash_error("Could not open playlist");
     return;
   }
@@ -682,8 +698,7 @@ void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set,
   rc_file_seek(file_handle, 0, SEEK_SET);
 
   m3u_contents = (char*)malloc((size_t)file_len + 1);
-  if (m3u_contents)
-  {
+  if (m3u_contents) {
     rc_file_read(file_handle, m3u_contents, (int)file_len);
     m3u_contents[file_len] = '\0';
 
@@ -696,23 +711,19 @@ void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set,
       while (isspace((int)*ptr))
         ++ptr;
 
-      if (*ptr == '#')
-      {
+      if (*ptr == '#') {
         /* ignore comment unless it's the special SAVEDISK extension */
-        if (memcmp(ptr, "#SAVEDISK:", 10) == 0)
-        {
+        if (memcmp(ptr, "#SAVEDISK:", 10) == 0) {
           /* get the path to the save disk from the frontend, assign it a bogus hash so
            * it doesn't get hashed later */
-          if (get_image_path(index, image_path, sizeof(image_path)))
-          {
+          if (get_image_path(index, image_path, sizeof(image_path))) {
             const char save_disk_hash[33] = "[SAVE DISK]";
             rc_libretro_hash_set_add(hash_set, image_path, -1, save_disk_hash);
             ++index;
           }
         }
       }
-      else
-      {
+      else {
         /* non-empty line, tally a file */
         ++index;
       }
@@ -726,8 +737,7 @@ void rc_libretro_hash_set_init(struct rc_libretro_hash_set_t* hash_set,
     free(m3u_contents);
   }
 
-  if (hash_set->entries_count > 0)
-  {
+  if (hash_set->entries_count > 0) {
     /* at least one save disk was found. make sure the core supports the #SAVEDISK: extension by
      * asking for the last expected disk. if it's not found, assume no #SAVEDISK: support */
     if (!get_image_path(index - 1, image_path, sizeof(image_path)))
@@ -759,13 +769,10 @@ void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set,
   struct rc_libretro_hash_entry_t* scan;
   struct rc_libretro_hash_entry_t* stop = hash_set->entries + hash_set->entries_count;;
 
-  if (path_djb2)
-  {
+  if (path_djb2) {
     /* attempt to match the path */
-    for (scan = hash_set->entries; scan < stop; ++scan)
-    {
-      if (scan->path_djb2 == path_djb2)
-      {
+    for (scan = hash_set->entries; scan < stop; ++scan) {
+      if (scan->path_djb2 == path_djb2) {
         entry = scan;
         break;
       }
@@ -775,19 +782,20 @@ void rc_libretro_hash_set_add(struct rc_libretro_hash_set_t* hash_set,
   if (!entry)
   {
     /* entry not found, allocate a new one */
-    if (hash_set->entries_size == 0)
-    {
+    if (hash_set->entries_size == 0) {
       hash_set->entries_size = 4;
       hash_set->entries = (struct rc_libretro_hash_entry_t*)
           malloc(hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t));
     }
-    else if (hash_set->entries_count == hash_set->entries_size)
-    {
+    else if (hash_set->entries_count == hash_set->entries_size) {
       hash_set->entries_size += 4;
       hash_set->entries = (struct rc_libretro_hash_entry_t*)realloc(hash_set->entries,
           hash_set->entries_size * sizeof(struct rc_libretro_hash_entry_t));
     }
 
+    if (hash_set->entries == NULL) /* unexpected, but better than crashing */
+      return;
+
     entry = hash_set->entries + hash_set->entries_count++;
   }
 
@@ -802,8 +810,7 @@ const char* rc_libretro_hash_set_get_hash(const struct rc_libretro_hash_set_t* h
   const uint32_t path_djb2 = rc_libretro_djb2(path);
   struct rc_libretro_hash_entry_t* scan = hash_set->entries;
   struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count;
-  for (; scan < stop; ++scan)
-  {
+  for (; scan < stop; ++scan) {
     if (scan->path_djb2 == path_djb2)
       return scan->hash;
   }
@@ -815,8 +822,7 @@ int rc_libretro_hash_set_get_game_id(const struct rc_libretro_hash_set_t* hash_s
 {
   struct rc_libretro_hash_entry_t* scan = hash_set->entries;
   struct rc_libretro_hash_entry_t* stop = scan + hash_set->entries_count;
-  for (; scan < stop; ++scan)
-  {
+  for (; scan < stop; ++scan) {
     if (memcmp(scan->hash, hash, sizeof(scan->hash)) == 0)
       return scan->game_id;
   }
diff --git a/dep/rcheevos/src/rc_version.h b/dep/rcheevos/src/rc_version.h
index 6aa57af6c..daf57e1cb 100644
--- a/dep/rcheevos/src/rc_version.h
+++ b/dep/rcheevos/src/rc_version.h
@@ -8,7 +8,7 @@
 RC_BEGIN_C_DECLS
 
 #define RCHEEVOS_VERSION_MAJOR 11
-#define RCHEEVOS_VERSION_MINOR 0
+#define RCHEEVOS_VERSION_MINOR 1
 #define RCHEEVOS_VERSION_PATCH 0
 
 #define RCHEEVOS_MAKE_VERSION(major, minor, patch) (major * 1000000 + minor * 1000 + patch)
diff --git a/dep/rcheevos/src/rcheevos/consoleinfo.c b/dep/rcheevos/src/rcheevos/consoleinfo.c
index 95eb406cf..427db73b2 100644
--- a/dep/rcheevos/src/rcheevos/consoleinfo.c
+++ b/dep/rcheevos/src/rcheevos/consoleinfo.c
@@ -567,6 +567,29 @@ static const rc_memory_region_t _rc_memory_regions_msx[] = {
 };
 static const rc_memory_regions_t rc_memory_regions_msx = { _rc_memory_regions_msx, 1 };
 
+/* ===== MS DOS ===== */
+static const rc_memory_region_t _rc_memory_regions_ms_dos[] = {
+    /* DOS emulators split the 640 KB conventional memory into two regions.
+     * First the part of the conventional memory given to the running game at $000000.
+     * The part of the conventional memory containing DOS and BIOS controlled memory
+     * is at $100000. The length of these can vary depending on the hardware
+     * and DOS version (or emulated DOS shell).
+     * These first two regions will only ever total to 640 KB but the regions map
+     * to 1 MB bounds to make resulting memory addresses more readable.
+     * When emulating a game not under DOS (so called 'PC Booter' games), the entirety
+     * of the 640 KB conventional memory block will be at $000000.
+     */
+    { 0x00000000U, 0x0009FFFFU, 0x00000000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Game Conventional Memory" },
+    { 0x000A0000U, 0x000FFFFFU, 0x000A0000U, RC_MEMORY_TYPE_UNUSED, "Padding to align OS Conventional Memory" },
+    { 0x00100000U, 0x0019FFFFU, 0x00100000U, RC_MEMORY_TYPE_SYSTEM_RAM, "OS Conventional Memory" },
+    { 0x001A0000U, 0x001FFFFFU, 0x001A0000U, RC_MEMORY_TYPE_UNUSED, "Padding to align Expanded Memory" },
+    /* Last is all the expanded memory which for now we map up to 64 MB which should be
+     * enough for the games we want to cover. An emulator might emulate more than that.
+     */
+    { 0x00200000U, 0x041FFFFFU, 0x00200000U, RC_MEMORY_TYPE_SYSTEM_RAM, "Expanded Memory" }
+};
+static const rc_memory_regions_t rc_memory_regions_ms_dos = { _rc_memory_regions_ms_dos, 5 };
+
 /* ===== Neo Geo Pocket ===== */
 /* http://neopocott.emuunlim.com/docs/tech-11.txt */
 static const rc_memory_region_t _rc_memory_regions_neo_geo_pocket[] = {
@@ -972,6 +995,9 @@ const rc_memory_regions_t* rc_console_memory_regions(uint32_t console_id)
     case RC_CONSOLE_MSX:
       return &rc_memory_regions_msx;
 
+    case RC_CONSOLE_MS_DOS:
+      return &rc_memory_regions_ms_dos;
+
     case RC_CONSOLE_NEOGEO_POCKET:
       return &rc_memory_regions_neo_geo_pocket;
 
diff --git a/dep/rcheevos/src/rcheevos/rc_internal.h b/dep/rcheevos/src/rcheevos/rc_internal.h
index 8dac46146..135423980 100644
--- a/dep/rcheevos/src/rcheevos/rc_internal.h
+++ b/dep/rcheevos/src/rcheevos/rc_internal.h
@@ -2,7 +2,7 @@
 #define RC_INTERNAL_H
 
 #include "rc_runtime_types.h"
-#include "../rc_util.h"
+#include "rc_util.h"
 
 RC_BEGIN_C_DECLS
 
diff --git a/dep/rcheevos/src/rcheevos/runtime_progress.c b/dep/rcheevos/src/rcheevos/runtime_progress.c
index d301377db..fd951dbd5 100644
--- a/dep/rcheevos/src/rcheevos/runtime_progress.c
+++ b/dep/rcheevos/src/rcheevos/runtime_progress.c
@@ -1,7 +1,7 @@
 #include "rc_runtime.h"
 #include "rc_internal.h"
 
-#include "../rc_util.h"
+#include "rc_util.h"
 #include "../rhash/md5.h"
 
 #include <stdlib.h>
diff --git a/dep/rcheevos/src/rhash/hash.c b/dep/rcheevos/src/rhash/hash.c
index 382076c20..5d65512b6 100644
--- a/dep/rcheevos/src/rhash/hash.c
+++ b/dep/rcheevos/src/rhash/hash.c
@@ -16,6 +16,7 @@
 #define MAX_BUFFER_SIZE 64 * 1024 * 1024
 
 const char* rc_path_get_filename(const char* path);
+static int rc_hash_whole_file(char hash[33], const char* path);
 
 /* ===================================================== */
 
@@ -511,8 +512,8 @@ static int rc_hash_cd_file(md5_state_t* md5, void* track_handle, uint32_t sector
     verbose_message_callback(message);
   }
 
-  if (size < (unsigned)num_read)
-    size = (unsigned)num_read;
+  if (size < (unsigned)num_read) /* we read a whole sector - only hash the part containing file data */
+    num_read = (size_t)size;
 
   do
   {
@@ -679,6 +680,293 @@ static int rc_hash_7800(char hash[33], const uint8_t* buffer, size_t buffer_size
   return rc_hash_buffer(hash, buffer, buffer_size);
 }
 
+struct rc_hash_zip_idx
+{
+  size_t length;
+  uint8_t* data;
+};
+
+static int rc_hash_zip_idx_sort(const void* a, const void* b)
+{
+  struct rc_hash_zip_idx *A = (struct rc_hash_zip_idx*)a, *B = (struct rc_hash_zip_idx*)b;
+  size_t len = (A->length < B->length ? A->length : B->length);
+  return memcmp(A->data, B->data, len);
+}
+
+static int rc_hash_zip_file(md5_state_t* md5, void* file_handle)
+{
+  uint8_t buf[2048], *alloc_buf, *cdir_start, *cdir_max, *cdir, *hashdata, eocdirhdr_size, cdirhdr_size;
+  uint32_t cdir_entry_len;
+  size_t sizeof_idx, indices_offset, alloc_size;
+  int64_t i_file, archive_size, ecdh_ofs, total_files, cdir_size, cdir_ofs;
+  struct rc_hash_zip_idx* hashindices, *hashindex;
+
+  rc_file_seek(file_handle, 0, SEEK_END);
+  archive_size = rc_file_tell(file_handle);
+
+  /* Basic sanity checks - reject files which are too small */
+  eocdirhdr_size = 22; /* the 'end of central directory header' is 22 bytes */
+  if (archive_size < eocdirhdr_size)
+    return rc_hash_error("ZIP is too small");
+
+  /* Macros used for reading ZIP and writing to a buffer for hashing (undefined again at the end of the function) */
+  #define RC_ZIP_READ_LE16(p) ((uint16_t)(((const uint8_t*)(p))[0]) | ((uint16_t)(((const uint8_t*)(p))[1]) << 8U))
+  #define RC_ZIP_READ_LE32(p) ((uint32_t)(((const uint8_t*)(p))[0]) | ((uint32_t)(((const uint8_t*)(p))[1]) << 8U) | ((uint32_t)(((const uint8_t*)(p))[2]) << 16U) | ((uint32_t)(((const uint8_t*)(p))[3]) << 24U))
+  #define RC_ZIP_READ_LE64(p) ((uint64_t)(((const uint8_t*)(p))[0]) | ((uint64_t)(((const uint8_t*)(p))[1]) << 8U) | ((uint64_t)(((const uint8_t*)(p))[2]) << 16U) | ((uint64_t)(((const uint8_t*)(p))[3]) << 24U) | ((uint64_t)(((const uint8_t*)(p))[4]) << 32U) | ((uint64_t)(((const uint8_t*)(p))[5]) << 40U) | ((uint64_t)(((const uint8_t*)(p))[6]) << 48U) | ((uint64_t)(((const uint8_t*)(p))[7]) << 56U))
+  #define RC_ZIP_WRITE_LE32(p,v) { ((uint8_t*)(p))[0] = (uint8_t)((uint32_t)(v) & 0xFF); ((uint8_t*)(p))[1] = (uint8_t)(((uint32_t)(v) >> 8) & 0xFF); ((uint8_t*)(p))[2] = (uint8_t)(((uint32_t)(v) >> 16) & 0xFF); ((uint8_t*)(p))[3] = (uint8_t)((uint32_t)(v) >> 24); }
+  #define RC_ZIP_WRITE_LE64(p,v) { ((uint8_t*)(p))[0] = (uint8_t)((uint64_t)(v) & 0xFF); ((uint8_t*)(p))[1] = (uint8_t)(((uint64_t)(v) >> 8) & 0xFF); ((uint8_t*)(p))[2] = (uint8_t)(((uint64_t)(v) >> 16) & 0xFF); ((uint8_t*)(p))[3] = (uint8_t)(((uint64_t)(v) >> 24) & 0xFF); ((uint8_t*)(p))[4] = (uint8_t)(((uint64_t)(v) >> 32) & 0xFF); ((uint8_t*)(p))[5] = (uint8_t)(((uint64_t)(v) >> 40) & 0xFF); ((uint8_t*)(p))[6] = (uint8_t)(((uint64_t)(v) >> 48) & 0xFF); ((uint8_t*)(p))[7] = (uint8_t)((uint64_t)(v) >> 56); }
+
+  /* Find the end of central directory record by scanning the file from the end towards the beginning */
+  for (ecdh_ofs = archive_size - sizeof(buf); ; ecdh_ofs -= (sizeof(buf) - 3))
+  {
+    int i, n = sizeof(buf);
+    if (ecdh_ofs < 0)
+      ecdh_ofs = 0;
+    if (n > archive_size)
+      n = (int)archive_size;
+    rc_file_seek(file_handle, ecdh_ofs, SEEK_SET);
+    if (rc_file_read(file_handle, buf, n) != (size_t)n)
+      return rc_hash_error("ZIP read error");
+    for (i = n - 4; i >= 0; --i)
+      if (RC_ZIP_READ_LE32(buf + i) == 0x06054b50) /* end of central directory header signature */
+        break;
+    if (i >= 0)
+    {
+      ecdh_ofs += i;
+      break;
+    }
+    if (!ecdh_ofs || (archive_size - ecdh_ofs) >= (0xFFFF + eocdirhdr_size))
+      return rc_hash_error("Failed to find ZIP central directory");
+  }
+
+  /* Read and verify the end of central directory record. */
+  rc_file_seek(file_handle, ecdh_ofs, SEEK_SET);
+  if (rc_file_read(file_handle, buf, eocdirhdr_size) != eocdirhdr_size)
+    return rc_hash_error("Failed to read ZIP central directory");
+
+  /* Read central dir information from end of central directory header */
+  total_files = RC_ZIP_READ_LE16(buf + 0x0A);
+  cdir_size   = RC_ZIP_READ_LE32(buf + 0x0C);
+  cdir_ofs    = RC_ZIP_READ_LE32(buf + 0x10);
+
+  /* Check if this is a Zip64 file. In the block of code below:
+   * - 20 is the size of the ZIP64 end of central directory locator
+   * - 56 is the size of the ZIP64 end of central directory header
+   */
+  if ((cdir_ofs == 0xFFFFFFFF || cdir_size == 0xFFFFFFFF || total_files == 0xFFFF) && ecdh_ofs >= (20 + 56))
+  {
+    /* Read the ZIP64 end of central directory locator if it actually exists */
+    rc_file_seek(file_handle, ecdh_ofs - 20, SEEK_SET);
+    if (rc_file_read(file_handle, buf, 20) == 20 && RC_ZIP_READ_LE32(buf) == 0x07064b50) /* locator signature */
+    {
+      /* Found the locator, now read the actual ZIP64 end of central directory header */ 
+      int64_t ecdh64_ofs = (int64_t)RC_ZIP_READ_LE64(buf + 0x08);
+      if (ecdh64_ofs <= (archive_size - 56))
+      {
+        rc_file_seek(file_handle, ecdh64_ofs, SEEK_SET);
+        if (rc_file_read(file_handle, buf, 56) == 56 && RC_ZIP_READ_LE32(buf) == 0x06064b50) /* header signature */
+        {
+          total_files = RC_ZIP_READ_LE64(buf + 0x20);
+          cdir_size   = RC_ZIP_READ_LE64(buf + 0x28);
+          cdir_ofs    = RC_ZIP_READ_LE64(buf + 0x30);
+        }
+      }
+    }
+  }
+
+  /* Basic verificaton of central directory (limit to a 256MB content directory) */
+  cdirhdr_size = 46; /* the 'central directory header' is 46 bytes */
+  if ((cdir_size >= 0x10000000) || (cdir_size < total_files * cdirhdr_size) || ((cdir_ofs + cdir_size) > archive_size))
+    return rc_hash_error("Central directory of ZIP file is invalid");
+
+  /* Allocate once for both directory and our temporary sort index (memory aligned to sizeof(rc_hash_zip_idx)) */
+  sizeof_idx = sizeof(struct rc_hash_zip_idx);
+  indices_offset = (size_t)((cdir_size + sizeof_idx - 1) / sizeof_idx * sizeof_idx);
+  alloc_size = (size_t)(indices_offset + total_files * sizeof_idx);
+  alloc_buf = (uint8_t*)malloc(alloc_size);
+
+  /* Read entire central directory to a buffer */
+  if (!alloc_buf)
+    return rc_hash_error("Could not allocate temporary buffer");
+  rc_file_seek(file_handle, cdir_ofs, SEEK_SET);
+  if ((int64_t)rc_file_read(file_handle, alloc_buf, (int)cdir_size) != cdir_size)
+  {
+    free(alloc_buf);
+    return rc_hash_error("Failed to read central directory of ZIP file");
+  }
+
+  cdir_start = alloc_buf;
+  cdir_max = cdir_start + cdir_size - cdirhdr_size;
+  cdir = cdir_start;
+
+  /* Write our temporary hash data to the same buffer we read the central directory from.
+   * We can do that because the amount of data we keep for each file is guaranteed to be less than the file record.
+   */
+  hashdata = alloc_buf;
+  hashindices = (struct rc_hash_zip_idx*)(alloc_buf + indices_offset);
+  hashindex = hashindices;
+
+  /* Now process the central directory file records */
+  for (i_file = 0, cdir = cdir_start; i_file < total_files && cdir >= cdir_start && cdir <= cdir_max; i_file++, cdir += cdir_entry_len)
+  {
+    const uint8_t *name, *name_end;
+    uint32_t signature     = RC_ZIP_READ_LE32(cdir + 0x00);
+    uint32_t method        = RC_ZIP_READ_LE16(cdir + 0x0A);
+    uint32_t crc32         = RC_ZIP_READ_LE32(cdir + 0x10);
+    uint64_t comp_size     = RC_ZIP_READ_LE32(cdir + 0x14);
+    uint64_t decomp_size   = RC_ZIP_READ_LE32(cdir + 0x18);
+    uint32_t filename_len  = RC_ZIP_READ_LE16(cdir + 0x1C);
+    int32_t  extra_len     = RC_ZIP_READ_LE16(cdir + 0x1E);
+    int32_t  comment_len   = RC_ZIP_READ_LE16(cdir + 0x20);
+    uint64_t local_hdr_ofs = RC_ZIP_READ_LE32(cdir + 0x2A);
+    cdir_entry_len = cdirhdr_size + filename_len + extra_len + comment_len;
+
+    if (signature != 0x02014b50) /* expected central directory entry signature */
+      break;
+
+    /* Handle Zip64 fields */
+    if (decomp_size == 0xFFFFFFFF || comp_size == 0xFFFFFFFF || local_hdr_ofs == 0xFFFFFFFF)
+    {
+      int invalid = 0;
+      const uint8_t *x = cdir + cdirhdr_size + filename_len, *xEnd, *field, *fieldEnd;
+      for (xEnd = x + extra_len; (x + (sizeof(uint16_t) * 2)) < xEnd; x = fieldEnd)
+      {
+        field = x + (sizeof(uint16_t) * 2);
+        fieldEnd = field + RC_ZIP_READ_LE16(x + 2);
+        if (RC_ZIP_READ_LE16(x) != 0x0001 || fieldEnd > xEnd)
+          continue; /* Not the Zip64 extended information extra field */
+
+        if (decomp_size == 0xFFFFFFFF)
+        {
+          if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { invalid = 1; break; }
+          decomp_size = RC_ZIP_READ_LE64(field);
+          field += sizeof(uint64_t);
+        }
+        if (comp_size == 0xFFFFFFFF)
+        {
+          if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { invalid = 1; break; }
+          comp_size = RC_ZIP_READ_LE64(field);
+          field += sizeof(uint64_t);
+        }
+        if (local_hdr_ofs == 0xFFFFFFFF)
+        {
+          if ((unsigned)(fieldEnd - field) < sizeof(uint64_t)) { invalid = 1; break; }
+          local_hdr_ofs = RC_ZIP_READ_LE64(field);
+          field += sizeof(uint64_t);
+        }
+        break;
+      }
+      if (invalid)
+      {
+        free(alloc_buf);
+        return rc_hash_error("Encountered invalid Zip64 file");
+      }
+    }
+
+    /* Basic sanity check on file record */
+    /* 30 is the length of the local directory header preceeding the compressed data */
+    if ((!method && decomp_size != comp_size) || (decomp_size && !comp_size) || ((local_hdr_ofs + 30 + comp_size) > (uint64_t)archive_size))
+    {
+      free(alloc_buf);
+      return rc_hash_error("Encountered invalid entry in ZIP central directory");
+    }
+
+    /* Write the pointer and length of the data we record about this file */
+    hashindex->data = hashdata;
+    hashindex->length = filename_len + 1 + 4 + 8;
+    hashindex++;
+
+    /* Convert and store the file name in the hash data buffer */
+    for (name = (cdir + cdirhdr_size), name_end = name + filename_len; name != name_end; name++)
+    {
+      *(hashdata++) =
+        (*name == '\\' ? '/' : /* convert back-slashes to regular slashes */
+        (*name >= 'A' && *name <= 'Z') ? (*name | 0x20) : /* convert upper case letters to lower case */
+        *name); /* else use the byte as-is */
+    }
+
+    /* Add zero terminator, CRC32 and decompressed size to the hash data buffer */
+    *(hashdata++) = '\0';
+    RC_ZIP_WRITE_LE32(hashdata, crc32);
+    hashdata += 4;
+    RC_ZIP_WRITE_LE64(hashdata, decomp_size);
+    hashdata += 8;
+
+    if (verbose_message_callback)
+    {
+      char message[1024];
+      snprintf(message, sizeof(message), "File in ZIP: %.*s (%u bytes, CRC32 = %08X)", filename_len, (const char*)(cdir + cdirhdr_size), (unsigned)decomp_size, crc32);
+      verbose_message_callback(message);
+    }
+  }
+
+  if (verbose_message_callback)
+  {
+    char message[1024];
+    snprintf(message, sizeof(message), "Hashing %u files in ZIP archive", (unsigned)(hashindex - hashindices));
+    verbose_message_callback(message);
+  }
+
+  /* Sort the file list indices */
+  qsort(hashindices, (hashindex - hashindices), sizeof(struct rc_hash_zip_idx), rc_hash_zip_idx_sort);
+
+  /* Hash the data in the order of the now sorted indices */
+  for (; hashindices != hashindex; hashindices++)
+    md5_append(md5, hashindices->data, (int)hashindices->length);
+
+  free(alloc_buf);
+  return 1;
+
+  #undef RC_ZIP_READ_LE16
+  #undef RC_ZIP_READ_LE32
+  #undef RC_ZIP_READ_LE64
+  #undef RC_ZIP_WRITE_LE32
+  #undef RC_ZIP_WRITE_LE64
+}
+
+static int rc_hash_ms_dos(char hash[33], const char* path)
+{
+  md5_state_t md5;
+  size_t path_len;
+  int res;
+
+  void* file_handle = rc_file_open(path);
+  if (!file_handle)
+    return rc_hash_error("Could not open file");
+
+  /* hash the main content zip file first */
+  md5_init(&md5);
+  res = rc_hash_zip_file(&md5, file_handle);
+  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);
+}
+
 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 */
@@ -2083,6 +2371,7 @@ int rc_hash_generate_from_buffer(char hash[33], uint32_t console_id, const uint8
       return rc_hash_snes(hash, buffer, buffer_size);
 
     case RC_CONSOLE_NINTENDO_64:
+    case RC_CONSOLE_NINTENDO_3DS:
     case RC_CONSOLE_NINTENDO_DS:
     case RC_CONSOLE_NINTENDO_DSI:
       return rc_hash_file_from_buffer(hash, console_id, buffer, buffer_size);
@@ -2401,6 +2690,9 @@ int rc_hash_generate_from_file(char hash[33], uint32_t console_id, const char* p
     case RC_CONSOLE_GAMECUBE:
       return rc_hash_gamecube(hash, path);
 
+    case RC_CONSOLE_MS_DOS:
+      return rc_hash_ms_dos(hash, path);
+
     case RC_CONSOLE_NEO_GEO_CD:
       return rc_hash_neogeo_cd(hash, path);
 
@@ -2539,6 +2831,14 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char*
         }
         break;
 
+      case '3':
+        if (rc_path_compare_extension(ext, "3ds") ||
+            rc_path_compare_extension(ext, "3dsx"))
+        {
+          iterator->consoles[0] = RC_CONSOLE_NINTENDO_3DS;
+        }
+        break;
+
       case '7':
         if (rc_path_compare_extension(ext, "7z"))
         {
@@ -2562,6 +2862,11 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char*
         {
           iterator->consoles[0] = RC_CONSOLE_ATARI_7800;
         }
+        else if (rc_path_compare_extension(ext, "app") ||
+                 rc_path_compare_extension(ext, "axf"))
+        {
+          iterator->consoles[0] = RC_CONSOLE_NINTENDO_3DS;
+        }
         break;
 
       case 'b':
@@ -2647,6 +2952,12 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char*
         {
           iterator->consoles[0] = RC_CONSOLE_SUPER_CASSETTEVISION;
         }
+        else if (rc_path_compare_extension(ext, "cci") ||
+                 rc_path_compare_extension(ext, "cia") ||
+                 rc_path_compare_extension(ext, "cxi"))
+        {
+          iterator->consoles[0] = RC_CONSOLE_NINTENDO_3DS;
+        }
         break;
 
       case 'd':
@@ -2663,6 +2974,19 @@ void rc_hash_initialize_iterator(struct rc_hash_iterator* iterator, const char*
           iterator->consoles[0] = RC_CONSOLE_PC8800;
           iterator->consoles[1] = RC_CONSOLE_SHARPX1;
         }
+        else if (rc_path_compare_extension(ext, "dosz"))
+        {
+            iterator->consoles[0] = RC_CONSOLE_MS_DOS;
+        }
+        break;
+
+      case 'e':
+        if (rc_path_compare_extension(ext, "elf"))
+        {
+          /* This should probably apply to more consoles in the future */
+          /* Although in any case this just hashes the entire file */
+          iterator->consoles[0] = RC_CONSOLE_NINTENDO_3DS;
+        }
         break;
 
       case 'f':