Parcourir la source

Save and load local replays

Clément Wolf il y a 1 mois
Parent
commit
a12118cf46
43 fichiers modifiés avec 847 ajouts et 775 suppressions
  1. 9 1
      src/App/Osu/Bancho.cpp
  2. 6 7
      src/App/Osu/BanchoLeaderboard.cpp
  3. 17 12
      src/App/Osu/BanchoNetworking.cpp
  4. 3 52
      src/App/Osu/BanchoSubmitter.cpp
  5. 1 1
      src/App/Osu/BanchoSubmitter.h
  6. 2 2
      src/App/Osu/Osu.cpp
  7. 3 3
      src/App/Osu/Osu.h
  8. 10 7
      src/App/Osu/OsuBeatmap.cpp
  9. 1 1
      src/App/Osu/OsuBeatmap.h
  10. 1 1
      src/App/Osu/OsuChat.cpp
  11. 280 312
      src/App/Osu/OsuDatabase.cpp
  12. 4 102
      src/App/Osu/OsuDatabase.h
  13. 2 2
      src/App/Osu/OsuHUD.cpp
  14. 2 2
      src/App/Osu/OsuMainMenu.cpp
  15. 1 1
      src/App/Osu/OsuModSelector.cpp
  16. 12 12
      src/App/Osu/OsuRankingScreen.cpp
  17. 3 3
      src/App/Osu/OsuRankingScreen.h
  18. 153 7
      src/App/Osu/OsuReplay.cpp
  19. 5 0
      src/App/Osu/OsuReplay.h
  20. 1 1
      src/App/Osu/OsuRichPresence.cpp
  21. 2 2
      src/App/Osu/OsuRoom.cpp
  22. 15 15
      src/App/Osu/OsuScore.cpp
  23. 6 19
      src/App/Osu/OsuScore.h
  24. 126 131
      src/App/Osu/OsuSongBrowser.cpp
  25. 3 3
      src/App/Osu/OsuSongBrowser.h
  26. 1 1
      src/App/Osu/OsuUIRankingScreenRankingPanel.cpp
  27. 1 1
      src/App/Osu/OsuUIRankingScreenRankingPanel.h
  28. 2 2
      src/App/Osu/OsuUISongBrowserButton.cpp
  29. 3 3
      src/App/Osu/OsuUISongBrowserButton.h
  30. 3 3
      src/App/Osu/OsuUISongBrowserCollectionButton.cpp
  31. 1 1
      src/App/Osu/OsuUISongBrowserCollectionButton.h
  32. 1 1
      src/App/Osu/OsuUISongBrowserInfoLabel.cpp
  33. 13 27
      src/App/Osu/OsuUISongBrowserScoreButton.cpp
  34. 6 6
      src/App/Osu/OsuUISongBrowserScoreButton.h
  35. 5 5
      src/App/Osu/OsuUISongBrowserSongButton.cpp
  36. 3 3
      src/App/Osu/OsuUISongBrowserSongButton.h
  37. 4 5
      src/App/Osu/OsuUISongBrowserSongDifficultyButton.cpp
  38. 1 1
      src/App/Osu/OsuUISongBrowserSongDifficultyButton.h
  39. 1 1
      src/App/Osu/OsuUISongBrowserUserButton.cpp
  40. 12 15
      src/App/Osu/OsuUserStatsScreen.cpp
  41. 1 1
      src/App/Osu/OsuVolumeOverlay.cpp
  42. 118 0
      src/App/Osu/Score.h
  43. 3 0
      src/Engine/Engine.cpp

+ 9 - 1
src/App/Osu/Bancho.cpp

@@ -27,7 +27,7 @@
 #include "OsuNotificationOverlay.h"
 #include "OsuOptionsMenu.h"
 #include "OsuRoom.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuUIAvatar.h"
 #include "OsuUIButton.h"
 #include "OsuUISongBrowserUserButton.h"
@@ -205,6 +205,14 @@ void handle_packet(Packet *packet) {
                 env->createDirectory(avatar_dir);
             }
 
+            std::stringstream ss2;
+            ss2 << MCENGINE_DATA_DIR "replays/";
+            ss2 << bancho.endpoint.toUtf8();
+            auto replays_dir = ss2.str();
+            if(!env->directoryExists(replays_dir)) {
+                env->createDirectory(replays_dir);
+            }
+
             // close your eyes
             SAFE_DELETE(bancho.osu->m_songBrowser2->m_userButton->m_avatar);
             bancho.osu->m_songBrowser2->m_userButton->m_avatar = new OsuUIAvatar(bancho.user_id, 0.f, 0.f, 0.f, 0.f);

+ 6 - 7
src/App/Osu/BanchoLeaderboard.cpp

@@ -13,10 +13,11 @@
 #include "Engine.h"
 #include "OsuDatabase.h"
 #include "OsuModSelector.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 
-OsuDatabase::Score parse_score(char *score_line) {
-    OsuDatabase::Score score;
+Score parse_score(char *score_line) {
+    Score score;
+    score.server = bancho.endpoint.toUtf8();
     score.isLegacyScore = true;
     score.isImportedLegacyScore = true;
     score.speedMultiplier = 1.0;
@@ -82,8 +83,6 @@ OsuDatabase::Score parse_score(char *score_line) {
     if(!str) return score;
     score.unixTimestamp = strtoul(str, NULL, 10);
 
-    // And we do nothing with has_replay
-
     // Set username for given user id, since we now know both
     auto user = get_user_info(score.player_id);
     user->name = score.playerName;
@@ -156,7 +155,7 @@ void process_leaderboard_response(Packet response) {
     //       you actually received the data if you plan on using it.
     OnlineMapInfo info = {0};
     MD5Hash beatmap_hash = (char *)response.extra;
-    std::vector<OsuDatabase::Score> scores;
+    std::vector<Score> scores;
     char *body = (char *)response.memory;
 
     char *ranked_status = strtok_x('|', &body);
@@ -195,7 +194,7 @@ void process_leaderboard_response(Packet response) {
 
     char *score_line = NULL;
     while((score_line = strtok_x('\n', &body))[0] != '\0') {
-        OsuDatabase::Score score = parse_score(score_line);
+        Score score = parse_score(score_line);
         score.md5hash = beatmap_hash;
         scores.push_back(score);
     }

+ 17 - 12
src/App/Osu/BanchoNetworking.cpp

@@ -18,7 +18,7 @@
 #include "OsuLobby.h"
 #include "OsuOptionsMenu.h"
 #include "OsuRoom.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuUIAvatar.h"
 #include "OsuUIButton.h"
 #include "miniz.h"
@@ -380,23 +380,28 @@ static void handle_api_response(Packet packet) {
         }
 
         case GET_REPLAY: {
-            auto replay_frames = OsuReplay::get_frames(packet.memory, packet.size);
-            if(replay_frames.empty()) {
+            if(packet.size == 0) {
                 // Most likely, 404
                 bancho.osu->m_notificationOverlay->addNotification("Failed to download replay");
                 break;
             }
 
-            bancho.osu->replay_info = *((ReplayExtraInfo *)packet.extra);
-            auto diff2_md5 = bancho.osu->replay_info.diff2_md5;
-            auto beatmap = bancho.osu->getSongBrowser()->getDatabase()->getBeatmapDifficulty(diff2_md5);
-            if(beatmap == nullptr) {
-                // XXX: Auto-download beatmap
-                bancho.osu->m_notificationOverlay->addNotification("Missing beatmap for this replay");
-            } else {
-                bancho.osu->getSongBrowser()->onDifficultySelected(beatmap, false);
-                bancho.osu->getSelectedBeatmap()->watch(replay_frames);
+            Score *score = (Score *)packet.extra;
+            std::stringstream replay_path;
+            replay_path << MCENGINE_DATA_DIR "replays/" << score->server << "/" << score->unixTimestamp
+                        << ".replay.lzma";
+
+            // XXX: this is blocking main thread
+            auto replay_path_str = replay_path.str();
+            FILE *replay_file = fopen(replay_path_str.c_str(), "wb");
+            if(replay_file == NULL) {
+                bancho.osu->m_notificationOverlay->addNotification("Failed to save replay");
+                break;
             }
+
+            fwrite(packet.memory, packet.size, 1, replay_file);
+            fclose(replay_file);
+            OsuReplay::load_and_watch(*score);
             break;
         }
 

+ 3 - 52
src/App/Osu/BanchoSubmitter.cpp

@@ -14,7 +14,7 @@
 #include "OsuDatabaseBeatmap.h"
 #include "base64.h"
 
-void submit_score(OsuDatabase::Score score) {
+void submit_score(Score score) {
     debugLog("Submitting score...\n");
     const char *GRADES[] = {"XH", "SH", "X", "S", "A", "B", "C", "D", "F", "N"};
 
@@ -160,57 +160,10 @@ void submit_score(OsuDatabase::Score score) {
         delete score_data_b64;
     }
     {
-        // osu!stable doesn't consider a replay valid unless it ends with this
-        score.replay.push_back(OsuReplay::Frame{
-            .cur_music_pos = -1,
-            .milliseconds_since_last_frame = -12345,
-            .x = 0,
-            .y = 0,
-            .key_flags = 0,
-        });
-
-        std::string replay_string;
-        for(auto frame : score.replay) {
-            auto frame_str = UString::format("%ld|%.4f|%.4f|%hhu,", frame.milliseconds_since_last_frame, frame.x,
-                                             frame.y, frame.key_flags);
-            replay_string.append(frame_str.toUtf8(), frame_str.lengthUtf8());
-        }
-
-        size_t s_compressed_data = replay_string.length();
-        compressed_data = (uint8_t *)malloc(s_compressed_data);
-        lzma_stream stream = LZMA_STREAM_INIT;
-        lzma_options_lzma options;
-        lzma_lzma_preset(&options, LZMA_PRESET_DEFAULT);
-        lzma_ret ret = lzma_alone_encoder(&stream, &options);
-        if(ret != LZMA_OK) {
-            debugLog("Failed to initialize lzma encoder: error %d\n", ret);
-            goto err;
-        }
-
-        stream.avail_in = replay_string.length();
-        stream.next_in = (const uint8_t *)replay_string.c_str();
-        stream.avail_out = s_compressed_data;
-        stream.next_out = compressed_data;
-        do {
-            ret = lzma_code(&stream, LZMA_FINISH);
-            if(ret == LZMA_OK) {
-                s_compressed_data *= 2;
-                compressed_data = (uint8_t *)realloc(compressed_data, s_compressed_data);
-                stream.avail_out = s_compressed_data - stream.total_out;
-                stream.next_out = compressed_data + stream.total_out;
-            } else if(ret != LZMA_STREAM_END) {
-                debugLog("Error while compressing replay: error %d\n", ret);
-                lzma_end(&stream);
-                goto err;
-            }
-        } while(ret != LZMA_STREAM_END);
-
-        s_compressed_data = stream.total_out;
-        lzma_end(&stream);
-
+        size_t s_compressed_data = 0;
+        OsuReplay::compress_frames(score.replay, &compressed_data, &s_compressed_data);
         if(s_compressed_data <= 24) {
             debugLog("Replay too small to submit! Compressed size: %d bytes\n", s_compressed_data);
-            debugLog("Replay frames: %s\n", replay_string.c_str());
             goto err;
         }
 
@@ -219,8 +172,6 @@ void submit_score(OsuDatabase::Score score) {
         curl_mime_name(part, "score");
         curl_mime_data(part, (const char *)compressed_data, s_compressed_data);
         free(compressed_data);
-
-        debugLog("Replay size: %d bytes (%d compressed)\n", replay_string.length(), s_compressed_data);
     }
 
     send_api_request(request);

+ 1 - 1
src/App/Osu/BanchoSubmitter.h

@@ -1,4 +1,4 @@
 #pragma once
 #include "OsuDatabase.h"
 
-void submit_score(OsuDatabase::Score score);
+void submit_score(Score score);

+ 2 - 2
src/App/Osu/Osu.cpp

@@ -55,7 +55,7 @@
 #include "OsuRoom.h"
 #include "OsuScore.h"
 #include "OsuSkin.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuTooltipOverlay.h"
 #include "OsuUIModSelectorModButton.h"
 #include "OsuUIUserContextMenu.h"
@@ -469,7 +469,7 @@ Osu::Osu(int instanceID) {
     m_tooltipOverlay = new OsuTooltipOverlay(this);
     m_mainMenu = new OsuMainMenu(this);
     m_optionsMenu = new OsuOptionsMenu(this);
-    m_songBrowser2 = new OsuSongBrowser2(this);
+    m_songBrowser2 = new OsuSongBrowser(this);
     m_backgroundImageHandler = new OsuBackgroundImageHandler();
     m_modSelector = new OsuModSelector(this);
     m_rankingScreen = new OsuRankingScreen(this);

+ 3 - 3
src/App/Osu/Osu.h

@@ -24,7 +24,7 @@ class OsuMainMenu;
 class OsuPauseMenu;
 class OsuOptionsMenu;
 class OsuModSelector;
-class OsuSongBrowser2;
+class OsuSongBrowser;
 class OsuBackgroundImageHandler;
 class OsuRankingScreen;
 class OsuUserStatsScreen;
@@ -117,7 +117,7 @@ class Osu : public App, public MouseListener {
     OsuBeatmap *getSelectedBeatmap();
 
     inline OsuOptionsMenu *getOptionsMenu() const { return m_optionsMenu; }
-    inline OsuSongBrowser2 *getSongBrowser() const { return m_songBrowser2; }
+    inline OsuSongBrowser *getSongBrowser() const { return m_songBrowser2; }
     inline OsuBackgroundImageHandler *getBackgroundImageHandler() const { return m_backgroundImageHandler; }
     inline OsuSkin *getSkin() const { return m_skin; }
     inline OsuHUD *getHUD() const { return m_hud; }
@@ -263,7 +263,7 @@ class Osu : public App, public MouseListener {
     OsuRoom *m_room = nullptr;
     OsuPromptScreen *m_prompt = nullptr;
     OsuUIUserContextMenuScreen *m_user_actions = nullptr;
-    OsuSongBrowser2 *m_songBrowser2 = nullptr;
+    OsuSongBrowser *m_songBrowser2 = nullptr;
     OsuBackgroundImageHandler *m_backgroundImageHandler;
     OsuModSelector *m_modSelector;
     OsuRankingScreen *m_rankingScreen;

+ 10 - 7
src/App/Osu/OsuBeatmap.cpp

@@ -48,7 +48,7 @@
 #include "OsuSkin.h"
 #include "OsuSkinImage.h"
 #include "OsuSlider.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuSpinner.h"
 #include "ResourceManager.h"
 #include "SoundEngine.h"
@@ -696,9 +696,9 @@ void OsuBeatmap::deselect() {
     unloadObjects();
 }
 
-bool OsuBeatmap::watch(std::vector<OsuReplay::Frame> replay) {
+bool OsuBeatmap::watch(Score score) {
     // Replay is invalid
-    if(replay.size() < 3) {
+    if(score.replay.size() < 3) {
         return false;
     }
 
@@ -711,7 +711,7 @@ bool OsuBeatmap::watch(std::vector<OsuReplay::Frame> replay) {
     }
 
     m_bIsWatchingReplay = true;  // play() resets this to false
-    spectated_replay = std::move(replay);
+    spectated_replay = score.replay;
 
     env->setCursorVisible(true);
 
@@ -3445,7 +3445,7 @@ void OsuBeatmap::onBeforeStop(bool quit) {
     const bool isCheated = (m_osu->getModAuto() || (m_osu->getModAutopilot() && m_osu->getModRelax())) ||
                            m_osu->getScore()->isUnranked() || m_bIsWatchingReplay;
 
-    OsuDatabase::Score score;
+    Score score;
     score.isLegacyScore = false;
     score.isImportedLegacyScore = false;
     score.version = OsuScore::VERSION;
@@ -3459,7 +3459,7 @@ void OsuBeatmap::onBeforeStop(bool quit) {
         score.playerName = convar->getConVarByName("name")->getString();
     }
     score.passed = isComplete && !isZero && !m_osu->getScore()->hasDied();
-    score.grade = score.passed ? m_osu->getScore()->getGrade() : OsuScore::GRADE::GRADE_F;
+    score.grade = score.passed ? m_osu->getScore()->getGrade() : Score::Grade::F;
     score.diff2 = m_selectedDifficulty2;
     score.ragequit = quit;
     score.play_time_ms = m_iCurMusicPos / m_osu->getSpeedMultiplier();
@@ -3504,6 +3504,8 @@ void OsuBeatmap::onBeforeStop(bool quit) {
     }
 
     score.md5hash = m_selectedDifficulty2->getMD5Hash();  // NOTE: necessary for "Use Mods"
+    score.has_replay = true;
+    score.replay = live_replay;
 
     int scoreIndex = -1;
 
@@ -3511,8 +3513,9 @@ void OsuBeatmap::onBeforeStop(bool quit) {
         OsuRichPresence::onPlayEnd(m_osu, quit);
 
         if(bancho.submit_scores() && !isZero && vanilla) {
-            score.replay = live_replay;
+            score.server = bancho.endpoint.toUtf8();
             submit_score(score);
+            // XXX: Save online_score_id after getting submission result
         }
 
         if(score.passed) {

+ 1 - 1
src/App/Osu/OsuBeatmap.h

@@ -104,7 +104,7 @@ class OsuBeatmap {
                     // clicking on a beatmap)
     void selectDifficulty2(OsuDatabaseBeatmap *difficulty2);
     void deselect();  // stops + unloads the currently loaded music and deletes all hitobjects
-    bool watch(std::vector<OsuReplay::Frame> replay);
+    bool watch(Score score);
     bool play();
     void restart(bool quick = false);
     void pause(bool quitIfWaiting = true);

+ 1 - 1
src/App/Osu/OsuChat.cpp

@@ -18,7 +18,7 @@
 #include "OsuPromptScreen.h"
 #include "OsuRoom.h"
 #include "OsuSkin.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuUIButton.h"
 #include "OsuUIUserContextMenu.h"
 #include "RenderTarget.h"

+ 280 - 312
src/App/Osu/OsuDatabase.cpp

@@ -56,7 +56,7 @@ ConVar osu_scores_enabled("osu_scores_enabled", true, FCVAR_NONE);
 ConVar osu_scores_legacy_enabled("osu_scores_legacy_enabled", true, FCVAR_NONE, "load osu!'s scores.db");
 ConVar osu_scores_custom_enabled("osu_scores_custom_enabled", true, FCVAR_NONE, "load custom scores.db");
 ConVar osu_scores_custom_version("osu_scores_custom_version", 20210110, FCVAR_NONE,
-                                 "maximum supported custom scores.db/scoresvr.db version");
+                                 "maximum supported custom scores.db version");
 ConVar osu_scores_save_immediately("osu_scores_save_immediately", true, FCVAR_NONE,
                                    "write scores.db as soon as a new score is added");
 ConVar osu_scores_sort_by_pp("osu_scores_sort_by_pp", true, FCVAR_NONE, "display pp in score browser instead of score");
@@ -115,7 +115,7 @@ bool save_db(Packet *db, std::string path) {
 
 struct SortScoreByScore : public OsuDatabase::SCORE_SORTING_COMPARATOR {
     virtual ~SortScoreByScore() { ; }
-    bool operator()(OsuDatabase::Score const &a, OsuDatabase::Score const &b) const {
+    bool operator()(Score const &a, Score const &b) const {
         // first: score
         unsigned long long score1 = a.score;
         unsigned long long score2 = b.score;
@@ -135,7 +135,7 @@ struct SortScoreByScore : public OsuDatabase::SCORE_SORTING_COMPARATOR {
 
 struct SortScoreByCombo : public OsuDatabase::SCORE_SORTING_COMPARATOR {
     virtual ~SortScoreByCombo() { ; }
-    bool operator()(OsuDatabase::Score const &a, OsuDatabase::Score const &b) const {
+    bool operator()(Score const &a, Score const &b) const {
         // first: combo
         unsigned long long score1 = a.comboMax;
         unsigned long long score2 = b.comboMax;
@@ -161,7 +161,7 @@ struct SortScoreByCombo : public OsuDatabase::SCORE_SORTING_COMPARATOR {
 
 struct SortScoreByDate : public OsuDatabase::SCORE_SORTING_COMPARATOR {
     virtual ~SortScoreByDate() { ; }
-    bool operator()(OsuDatabase::Score const &a, OsuDatabase::Score const &b) const {
+    bool operator()(Score const &a, Score const &b) const {
         // first: time
         unsigned long long score1 = a.unixTimestamp;
         unsigned long long score2 = b.unixTimestamp;
@@ -175,7 +175,7 @@ struct SortScoreByDate : public OsuDatabase::SCORE_SORTING_COMPARATOR {
 
 struct SortScoreByMisses : public OsuDatabase::SCORE_SORTING_COMPARATOR {
     virtual ~SortScoreByMisses() { ; }
-    bool operator()(OsuDatabase::Score const &a, OsuDatabase::Score const &b) const {
+    bool operator()(Score const &a, Score const &b) const {
         // first: misses
         unsigned long long score1 = b.numMisses;  // swapped (lower numMisses is better)
         unsigned long long score2 = a.numMisses;
@@ -201,7 +201,7 @@ struct SortScoreByMisses : public OsuDatabase::SCORE_SORTING_COMPARATOR {
 
 struct SortScoreByAccuracy : public OsuDatabase::SCORE_SORTING_COMPARATOR {
     virtual ~SortScoreByAccuracy() { ; }
-    bool operator()(OsuDatabase::Score const &a, OsuDatabase::Score const &b) const {
+    bool operator()(Score const &a, Score const &b) const {
         // first: accuracy
         unsigned long long score1 =
             (unsigned long long)(OsuScore::calculateAccuracy(a.num300s, a.num100s, a.num50s, a.numMisses) * 10000.0f);
@@ -229,7 +229,7 @@ struct SortScoreByAccuracy : public OsuDatabase::SCORE_SORTING_COMPARATOR {
 
 struct SortScoreByPP : public OsuDatabase::SCORE_SORTING_COMPARATOR {
     virtual ~SortScoreByPP() { ; }
-    bool operator()(OsuDatabase::Score const &a, OsuDatabase::Score const &b) const {
+    bool operator()(Score const &a, Score const &b) const {
         // first: pp
         unsigned long long score1 = (unsigned long long)std::max(a.pp * 100.0f, 0.0f);
         unsigned long long score2 = (unsigned long long)std::max(b.pp * 100.0f, 0.0f);
@@ -482,7 +482,7 @@ OsuDatabaseBeatmap *OsuDatabase::addBeatmap(std::string beatmapFolderPath) {
     return beatmap;
 }
 
-int OsuDatabase::addScore(MD5Hash beatmapMD5Hash, OsuDatabase::Score score) {
+int OsuDatabase::addScore(MD5Hash beatmapMD5Hash, Score score) {
     addScoreRaw(beatmapMD5Hash, score);
     sortScores(beatmapMD5Hash);
 
@@ -491,6 +491,23 @@ int OsuDatabase::addScore(MD5Hash beatmapMD5Hash, OsuDatabase::Score score) {
 
     if(osu_scores_save_immediately.getBool()) saveScores();
 
+    // XXX: this is blocking main thread
+    uint8_t *compressed_replay = NULL;
+    size_t s_compressed_replay = 0;
+    OsuReplay::compress_frames(score.replay, &compressed_replay, &s_compressed_replay);
+    if(s_compressed_replay > 0) {
+        std::string replay_path;
+        std::stringstream ss;
+        ss << MCENGINE_DATA_DIR "replays/" << score.unixTimestamp << ".replay.lzma";
+        replay_path = ss.str();
+
+        FILE *replay_file = fopen(replay_path.c_str(), "wb");
+        if(replay_file != NULL) {
+            fwrite(compressed_replay, s_compressed_replay, 1, replay_file);
+            fclose(replay_file);
+        }
+    }
+
     // return sorted index
     for(int i = 0; i < m_scores[beatmapMD5Hash].size(); i++) {
         if(m_scores[beatmapMD5Hash][i].unixTimestamp == score.unixTimestamp) return i;
@@ -499,7 +516,7 @@ int OsuDatabase::addScore(MD5Hash beatmapMD5Hash, OsuDatabase::Score score) {
     return -1;
 }
 
-void OsuDatabase::addScoreRaw(const MD5Hash &beatmapMD5Hash, const OsuDatabase::Score &score) {
+void OsuDatabase::addScoreRaw(const MD5Hash &beatmapMD5Hash, const Score &score) {
     m_scores[beatmapMD5Hash].push_back(score);
 
     // cheap dynamic recalculations for mcosu scores
@@ -510,7 +527,7 @@ void OsuDatabase::addScoreRaw(const MD5Hash &beatmapMD5Hash, const OsuDatabase::
         {
             // find score with maxPossibleCombo info
             int maxPossibleCombo = -1;
-            for(const OsuDatabase::Score &s : m_scores[beatmapMD5Hash]) {
+            for(const Score &s : m_scores[beatmapMD5Hash]) {
                 if(s.version > 20180722) {
                     if(s.maxPossibleCombo > 0) {
                         maxPossibleCombo = s.maxPossibleCombo;
@@ -521,7 +538,7 @@ void OsuDatabase::addScoreRaw(const MD5Hash &beatmapMD5Hash, const OsuDatabase::
 
             // set 'perfect' flag on all relevant old scores of that same beatmap
             if(maxPossibleCombo > 0) {
-                for(OsuDatabase::Score &s : m_scores[beatmapMD5Hash]) {
+                for(Score &s : m_scores[beatmapMD5Hash]) {
                     if(s.version <= 20180722 ||
                        s.maxPossibleCombo <
                            1)  // also set on scores which have broken maxPossibleCombo values for whatever reason
@@ -559,9 +576,7 @@ void OsuDatabase::sortScores(MD5Hash beatmapMD5Hash) {
         if(m_osu_songbrowser_scores_sortingtype_ref->getString() == m_scoreSortingMethods[i].name) {
             struct COMPARATOR_WRAPPER {
                 SCORE_SORTING_COMPARATOR *comp;
-                bool operator()(OsuDatabase::Score const &a, OsuDatabase::Score const &b) const {
-                    return comp->operator()(a, b);
-                }
+                bool operator()(Score const &a, Score const &b) const { return comp->operator()(a, b); }
             };
             COMPARATOR_WRAPPER comparatorWrapper;
             comparatorWrapper.comp = m_scoreSortingMethods[i].comparator;
@@ -1779,6 +1794,7 @@ void OsuDatabase::loadScores() {
     // load custom scores
     // NOTE: custom scores are loaded before legacy scores (because we want to be able to skip loading legacy scores
     // which were already previously imported at some point)
+    int nb_mcosu_scores = 0;
     size_t customScoresFileSize = 0;
     if(osu_scores_custom_enabled.getBool()) {
         const int maxSupportedCustomDbVersion = osu_scores_custom_version.getInt();
@@ -1794,7 +1810,6 @@ void OsuDatabase::loadScores() {
             debugLog("Custom scores: version = %i, numBeatmaps = %i\n", dbVersion, numBeatmaps);
 
             if(dbVersion <= maxSupportedCustomDbVersion) {
-                int scoreCounter = 0;
                 for(int b = 0; b < numBeatmaps; b++) {
                     auto hash_str = read_stdstring(&db);
                     const int numScores = read_int32(&db);
@@ -1815,268 +1830,227 @@ void OsuDatabase::loadScores() {
 
                     const MD5Hash md5hash = hash_str.c_str();
                     for(int s = 0; s < numScores; s++) {
-                        const unsigned char gamemode =
-                            read_byte(&db);  // NOTE: abused as isImportedLegacyScore flag (because I forgot to add a
-                                             // version cap to old builds)
-                        const int scoreVersion = read_int32(&db);
-                        bool isImportedLegacyScore = false;
-                        if(dbVersion == 20210103 && scoreVersion > 20190103) {
-                            isImportedLegacyScore = read_byte(&db);
-                        } else if(dbVersion > 20210103 && scoreVersion > 20190103) {
+                        Score sc;
+                        sc.isLegacyScore = false;
+                        sc.isImportedLegacyScore = false;
+                        sc.maxPossibleCombo = -1;
+                        sc.numHitObjects = -1;
+                        sc.numCircles = -1;
+                        sc.perfect = false;
+
+                        const unsigned char gamemode = read_byte(&db);  // NOTE: abused as isImportedLegacyScore flag
+                        sc.version = read_int32(&db);
+
+                        if(dbVersion == 20210103 && sc.version > 20190103) {
+                            sc.isImportedLegacyScore = read_byte(&db);
+                        } else if(dbVersion > 20210103 && sc.version > 20190103) {
                             // HACKHACK: for explanation see hackIsImportedLegacyScoreFlag
-                            isImportedLegacyScore = (gamemode & hackIsImportedLegacyScoreFlag);
+                            sc.isImportedLegacyScore = (gamemode & hackIsImportedLegacyScoreFlag);
                         }
-                        const uint64_t unixTimestamp = read_int64(&db);
+
+                        sc.unixTimestamp = read_int64(&db);
 
                         // default
-                        const UString playerName = read_string(&db);
+                        sc.playerName = read_string(&db);
 
-                        const short num300s = read_short(&db);
-                        const short num100s = read_short(&db);
-                        const short num50s = read_short(&db);
-                        const short numGekis = read_short(&db);
-                        const short numKatus = read_short(&db);
-                        const short numMisses = read_short(&db);
+                        sc.num300s = read_short(&db);
+                        sc.num100s = read_short(&db);
+                        sc.num50s = read_short(&db);
+                        sc.numGekis = read_short(&db);
+                        sc.numKatus = read_short(&db);
+                        sc.numMisses = read_short(&db);
 
-                        const unsigned long long score = read_int64(&db);
-                        const short maxCombo = read_short(&db);
-                        const int modsLegacy = read_int32(&db);
+                        sc.score = read_int64(&db);
+                        sc.comboMax = read_short(&db);
+                        sc.modsLegacy = read_int32(&db);
 
                         // custom
-                        const short numSliderBreaks = read_short(&db);
-                        const float pp = read_float32(&db);
-                        const float unstableRate = read_float32(&db);
-                        const float hitErrorAvgMin = read_float32(&db);
-                        const float hitErrorAvgMax = read_float32(&db);
-                        const float starsTomTotal = read_float32(&db);
-                        const float starsTomAim = read_float32(&db);
-                        const float starsTomSpeed = read_float32(&db);
-                        const float speedMultiplier = read_float32(&db);
-                        const float CS = read_float32(&db);
-                        const float AR = read_float32(&db);
-                        const float OD = read_float32(&db);
-                        const float HP = read_float32(&db);
-
-                        int maxPossibleCombo = -1;
-                        int numHitObjects = -1;
-                        int numCircles = -1;
-                        if(scoreVersion > 20180722) {
-                            maxPossibleCombo = read_int32(&db);
-                            numHitObjects = read_int32(&db);
-                            numCircles = read_int32(&db);
+                        sc.numSliderBreaks = read_short(&db);
+                        sc.pp = read_float32(&db);
+                        sc.unstableRate = read_float32(&db);
+                        sc.hitErrorAvgMin = read_float32(&db);
+                        sc.hitErrorAvgMax = read_float32(&db);
+                        sc.starsTomTotal = read_float32(&db);
+                        sc.starsTomAim = read_float32(&db);
+                        sc.starsTomSpeed = read_float32(&db);
+                        sc.speedMultiplier = read_float32(&db);
+                        sc.CS = read_float32(&db);
+                        sc.AR = read_float32(&db);
+                        sc.OD = read_float32(&db);
+                        sc.HP = read_float32(&db);
+
+                        if(sc.version > 20180722) {
+                            sc.maxPossibleCombo = read_int32(&db);
+                            sc.numHitObjects = read_int32(&db);
+                            sc.numCircles = read_int32(&db);
+                            sc.perfect = sc.comboMax >= sc.maxPossibleCombo;
                         }
 
-                        const auto experimentalMods = read_stdstring(&db);
+                        if(sc.version >= 20240412) {
+                            sc.has_replay = true;
+                            sc.online_score_id = read_int32(&db);
+                            sc.server = read_stdstring(&db);
+                        }
 
-                        if(gamemode == 0x0 ||
-                           (dbVersion > 20210103 &&
-                            scoreVersion > 20190103))  // gamemode filter (osu!standard) // HACKHACK: for explanation
-                                                       // see hackIsImportedLegacyScoreFlag
-                        {
-                            Score sc;
-
-                            sc.isLegacyScore = false;
-                            sc.isImportedLegacyScore = isImportedLegacyScore;
-                            sc.version = scoreVersion;
-                            sc.unixTimestamp = unixTimestamp;
-
-                            // default
-                            sc.playerName = playerName;
-
-                            sc.num300s = num300s;
-                            sc.num100s = num100s;
-                            sc.num50s = num50s;
-                            sc.numGekis = numGekis;
-                            sc.numKatus = numKatus;
-                            sc.numMisses = numMisses;
-                            sc.score = score;
-                            sc.comboMax = maxCombo;
-                            sc.perfect = (maxPossibleCombo > 0 && sc.comboMax > 0 && sc.comboMax >= maxPossibleCombo);
-                            sc.modsLegacy = modsLegacy;
-
-                            // custom
-                            sc.numSliderBreaks = numSliderBreaks;
-                            sc.pp = pp;
-                            sc.unstableRate = unstableRate;
-                            sc.hitErrorAvgMin = hitErrorAvgMin;
-                            sc.hitErrorAvgMax = hitErrorAvgMax;
-                            sc.starsTomTotal = starsTomTotal;
-                            sc.starsTomAim = starsTomAim;
-                            sc.starsTomSpeed = starsTomSpeed;
-                            sc.speedMultiplier = speedMultiplier;
-                            sc.CS = CS;
-                            sc.AR = AR;
-                            sc.OD = OD;
-                            sc.HP = HP;
-                            sc.maxPossibleCombo = maxPossibleCombo;
-                            sc.numHitObjects = numHitObjects;
-                            sc.numCircles = numCircles;
-                            sc.experimentalModsConVars = experimentalMods;
+                        sc.experimentalModsConVars = read_stdstring(&db);
 
+                        if(gamemode == 0x0 || (dbVersion > 20210103 && sc.version > 20190103)) {
                             // runtime
                             sc.sortHack = m_iSortHackCounter++;
                             sc.md5hash = md5hash;
 
                             addScoreRaw(md5hash, sc);
-                            scoreCounter++;
+                            nb_mcosu_scores++;
                         }
                     }
                 }
-                debugLog("Loaded %i individual scores.\n", scoreCounter);
             } else {
                 debugLog("Newer scores.db version is not backwards compatible with old clients.\n");
             }
-        } else {
-            debugLog("No custom scores found.\n");
         }
+
+        free(db.memory);
     }
 
     // load legacy osu scores
+    int nb_peppy_scores = 0;
     if(osu_scores_legacy_enabled.getBool()) {
         std::string scoresPath = osu_folder.getString().toUtf8();
         scoresPath.append("scores.db");
 
         Packet db = load_db(scoresPath);
-        if(db.size > 0) {
-            // HACKHACK: heuristic sanity check (some people have their
-            // osu!folder point directly to McOsu, which would break legacy
-            // score db loading here since there is no magic number)
-            if(db.size != customScoresFileSize) {
-                const int dbVersion = read_int32(&db);
-                const int numBeatmaps = read_int32(&db);
+        // HACKHACK: heuristic sanity check (some people have their osu!folder
+        // point directly to McOsu, which would break legacy score db loading
+        // here since there is no magic number)
+        if(db.size > 0 && db.size != customScoresFileSize) {
+            const int dbVersion = read_int32(&db);
+            const int numBeatmaps = read_int32(&db);
 
-                debugLog("Legacy scores: version = %i, numBeatmaps = %i\n", dbVersion, numBeatmaps);
+            debugLog("Legacy scores: version = %i, numBeatmaps = %i\n", dbVersion, numBeatmaps);
 
-                int scoreCounter = 0;
-                for(int b = 0; b < numBeatmaps; b++) {
-                    auto hash_str = read_stdstring(&db);
+            for(int b = 0; b < numBeatmaps; b++) {
+                auto hash_str = read_stdstring(&db);
 
-                    if(hash_str.size() < 32) {
-                        if(Osu::debug->getBool()) {
-                            debugLog("WARNING: Invalid score with md5hash.length() = %i!\n", hash_str.size());
-                        }
-                        continue;
-                    } else if(hash_str.size() > 32) {
-                        debugLog("ERROR: Corrupt score database/entry detected, stopping.\n");
-                        break;
+                if(hash_str.size() < 32) {
+                    if(Osu::debug->getBool()) {
+                        debugLog("WARNING: Invalid score with md5hash.length() = %i!\n", hash_str.size());
                     }
+                    continue;
+                } else if(hash_str.size() > 32) {
+                    debugLog("ERROR: Corrupt score database/entry detected, stopping.\n");
+                    break;
+                }
 
-                    const MD5Hash md5hash = hash_str.c_str();
-                    const int numScores = read_int32(&db);
+                const MD5Hash md5hash = hash_str.c_str();
+                const int numScores = read_int32(&db);
 
-                    if(Osu::debug->getBool())
-                        debugLog("Beatmap[%i]: md5hash = %s, numScores = %i\n", b, md5hash.toUtf8(), numScores);
+                if(Osu::debug->getBool())
+                    debugLog("Beatmap[%i]: md5hash = %s, numScores = %i\n", b, md5hash.toUtf8(), numScores);
+
+                for(int s = 0; s < numScores; s++) {
+                    Score sc;
+                    sc.server = "ppy.sh";
+                    sc.online_score_id = 0;
+                    sc.isLegacyScore = true;
+                    sc.isImportedLegacyScore = false;
+                    sc.numSliderBreaks = 0;
+                    sc.pp = 0.0f;
+                    sc.unstableRate = 0.0f;
+                    sc.hitErrorAvgMin = 0.0f;
+                    sc.hitErrorAvgMax = 0.0f;
+                    sc.starsTomTotal = 0.0f;
+                    sc.starsTomAim = 0.0f;
+                    sc.starsTomSpeed = 0.0f;
+                    sc.CS = 0.0f;
+                    sc.AR = 0.0f;
+                    sc.OD = 0.0f;
+                    sc.HP = 0.0f;
+                    sc.maxPossibleCombo = -1;
+                    sc.numHitObjects = -1;
+                    sc.numCircles = -1;
+
+                    const unsigned char gamemode = read_byte(&db);
+                    sc.version = read_int32(&db);
+                    skip_string(&db);  // beatmap hash (already have it)
+
+                    sc.playerName = read_string(&db);
+                    skip_string(&db);  // replay hash (don't use it)
+
+                    sc.num300s = read_short(&db);
+                    sc.num100s = read_short(&db);
+                    sc.num50s = read_short(&db);
+                    sc.numGekis = read_short(&db);
+                    sc.numKatus = read_short(&db);
+                    sc.numMisses = read_short(&db);
+
+                    int32_t score = read_int32(&db);
+                    sc.score = (score < 0 ? 0 : score);
+
+                    sc.comboMax = read_short(&db);
+                    sc.perfect = read_byte(&db);
+
+                    sc.modsLegacy = read_int32(&db);
+                    sc.speedMultiplier = 1.0f;
+                    if(sc.modsLegacy & ModFlags::HalfTime)
+                        sc.speedMultiplier = 0.75f;
+                    else if((sc.modsLegacy & ModFlags::DoubleTime) || (sc.modsLegacy & ModFlags::Nightcore))
+                        sc.speedMultiplier = 1.5f;
+
+                    skip_string(&db);  // hp graph
+
+                    uint64_t full_tms = read_int64(&db);
+                    sc.unixTimestamp = (full_tms - 621355968000000000) / 10000000;
+                    sc.legacyReplayTimestamp = full_tms - 504911232000000000;
+
+                    // Always -1, but let's skip it properly just in case
+                    int32_t old_replay_size = read_int32(&db);
+                    if(old_replay_size > 0) {
+                        db.pos += old_replay_size;
+                    }
 
-                    for(int s = 0; s < numScores; s++) {
-                        const unsigned char gamemode = read_byte(&db);
-                        const int scoreVersion = read_int32(&db);
-                        skip_string(&db);  // beatmap hash
-                        const UString playerName = read_string(&db);
-                        skip_string(&db);  // replay hash
-
-                        const short num300s = read_short(&db);
-                        const short num100s = read_short(&db);
-                        const short num50s = read_short(&db);
-                        const short numGekis = read_short(&db);
-                        const short numKatus = read_short(&db);
-                        const short numMisses = read_short(&db);
-
-                        const int score = read_int32(&db);
-                        const short maxCombo = read_short(&db);
-                        const bool perfect = read_byte(&db);
-
-                        const int mods = read_int32(&db);
-                        skip_string(&db);  // hp graph
-                        const long long ticksWindows = read_int64(&db);
-
-                        uint32_t replay_len = read_int32(&db);
-                        db.pos += replay_len;  // replayCompressed
-
-                        /*long long onlineScoreID = 0;*/
-                        if(scoreVersion >= 20140721)
-                            /*onlineScoreID = */ read_int64(&db);
-                        else if(scoreVersion >= 20121008)
-                            /*onlineScoreID = */ read_int32(&db);
-
-                        if(mods & ModFlags::Target) /*double totalAccuracy = */
-                            read_float64(&db);
-
-                        if(gamemode == 0x0)  // gamemode filter (osu!standard)
-                        {
-                            Score sc;
-
-                            sc.isLegacyScore = true;
-                            sc.isImportedLegacyScore = false;
-                            sc.version = scoreVersion;
-                            sc.unixTimestamp = (ticksWindows - 621355968000000000) / 10000000;
-
-                            // default
-                            sc.playerName = playerName;
-
-                            sc.num300s = num300s;
-                            sc.num100s = num100s;
-                            sc.num50s = num50s;
-                            sc.numGekis = numGekis;
-                            sc.numKatus = numKatus;
-                            sc.numMisses = numMisses;
-                            sc.score = (score < 0 ? 0 : score);
-                            sc.comboMax = maxCombo;
-                            sc.perfect = perfect;
-                            sc.modsLegacy = mods;
-
-                            // custom
-                            sc.numSliderBreaks = 0;
-                            sc.pp = 0.0f;
-                            sc.unstableRate = 0.0f;
-                            sc.hitErrorAvgMin = 0.0f;
-                            sc.hitErrorAvgMax = 0.0f;
-                            sc.starsTomTotal = 0.0f;
-                            sc.starsTomAim = 0.0f;
-                            sc.starsTomSpeed = 0.0f;
-                            sc.speedMultiplier =
-                                (mods & ModFlags::HalfTime
-                                     ? 0.75f
-                                     : (((mods & ModFlags::DoubleTime) || (mods & ModFlags::Nightcore)) ? 1.5f : 1.0f));
-                            sc.CS = 0.0f;
-                            sc.AR = 0.0f;
-                            sc.OD = 0.0f;
-                            sc.HP = 0.0f;
-                            sc.maxPossibleCombo = -1;
-                            sc.numHitObjects = -1;
-                            sc.numCircles = -1;
-                            // sc.experimentalModsConVars = "";
+                    // Just assume we have the replay ¯\_(ツ)_/¯
+                    sc.has_replay = true;
 
-                            // runtime
-                            sc.sortHack = m_iSortHackCounter++;
-                            sc.md5hash = md5hash;
+                    if(sc.version >= 20140721)
+                        sc.online_score_id = read_int64(&db);
+                    else if(sc.version >= 20121008)
+                        sc.online_score_id = read_int32(&db);
 
-                            scoreCounter++;
+                    if(sc.modsLegacy & ModFlags::Target) /*double totalAccuracy = */
+                        read_float64(&db);
 
-                            // NOTE: avoid adding an already imported legacy score (since that just spams the
-                            // scorebrowser with useless information)
-                            bool isScoreAlreadyImported = false;
-                            {
-                                const std::vector<OsuDatabase::Score> &otherScores = m_scores[sc.md5hash];
+                    if(gamemode == 0) {  // gamemode filter (osu!standard)
+                        // runtime
+                        sc.sortHack = m_iSortHackCounter++;
+                        sc.md5hash = md5hash;
 
-                                for(size_t s = 0; s < otherScores.size(); s++) {
-                                    if(sc.isLegacyScoreEqualToImportedLegacyScore(otherScores[s])) {
-                                        isScoreAlreadyImported = true;
-                                        break;
-                                    }
+                        nb_peppy_scores++;
+
+                        // NOTE: avoid adding an already imported legacy score (since that just spams the
+                        // scorebrowser with useless information)
+                        bool isScoreAlreadyImported = false;
+                        {
+                            const std::vector<Score> &otherScores = m_scores[sc.md5hash];
+
+                            for(size_t s = 0; s < otherScores.size(); s++) {
+                                if(sc.isLegacyScoreEqualToImportedLegacyScore(otherScores[s])) {
+                                    isScoreAlreadyImported = true;
+                                    break;
                                 }
                             }
-
-                            if(!isScoreAlreadyImported) addScoreRaw(md5hash, sc);
                         }
+
+                        if(!isScoreAlreadyImported) addScoreRaw(md5hash, sc);
                     }
                 }
-                debugLog("Loaded %i individual scores.\n", scoreCounter);
-            } else
-                debugLog("Not loading legacy scores because filesize matches custom scores.\n");
-        } else
-            debugLog("No legacy scores found.\n");
+            }
+        }
+
+        free(db.memory);
     }
 
+    debugLog("Loaded %i scores from McOsu and %i scores from osu!stable.\n", nb_mcosu_scores, nb_peppy_scores);
+
     if(m_scores.size() > 0) m_bScoresLoaded = true;
 }
 
@@ -2084,109 +2058,103 @@ void OsuDatabase::saveScores() {
     if(!m_bDidScoresChangeForSave) return;
     m_bDidScoresChangeForSave = false;
 
+    if(m_scores.empty()) return;
     const int dbVersion = osu_scores_custom_version.getInt();
     const unsigned char hackIsImportedLegacyScoreFlag =
         0xA9;  // TODO: remove this once all builds on steam (even previous-version) have loading version cap logic
 
-    if(m_scores.size() > 0) {
-        debugLog("Osu: Saving scores ...\n");
+    debugLog("Osu: Saving scores ...\n");
 
-        Packet db;
+    Packet db;
 
-        const double startTime = engine->getTimeReal();
+    const double startTime = engine->getTimeReal();
 
-        // count number of beatmaps with valid scores
-        int numBeatmaps = 0;
-        for(std::unordered_map<MD5Hash, std::vector<Score>>::iterator it = m_scores.begin(); it != m_scores.end();
-            ++it) {
-            for(int i = 0; i < it->second.size(); i++) {
-                if(!it->second[i].isLegacyScore) {
-                    numBeatmaps++;
-                    break;
-                }
+    // count number of beatmaps with valid scores
+    int numBeatmaps = 0;
+    for(std::unordered_map<MD5Hash, std::vector<Score>>::iterator it = m_scores.begin(); it != m_scores.end(); ++it) {
+        for(int i = 0; i < it->second.size(); i++) {
+            if(!it->second[i].isLegacyScore) {
+                numBeatmaps++;
+                break;
             }
         }
+    }
 
-        // write header
-        write_int32(&db, dbVersion);
-        write_int32(&db, numBeatmaps);
-
-        // write scores for each beatmap
-        for(std::unordered_map<MD5Hash, std::vector<Score>>::iterator it = m_scores.begin(); it != m_scores.end();
-            ++it) {
-            int numNonLegacyScores = 0;
-            for(int i = 0; i < it->second.size(); i++) {
-                if(!it->second[i].isLegacyScore) numNonLegacyScores++;
-            }
-
-            if(numNonLegacyScores > 0) {
-                write_string(&db, it->first.hash);     // md5hash
-                write_int32(&db, numNonLegacyScores);  // numScores
-
-                for(int i = 0; i < it->second.size(); i++) {
-                    if(!it->second[i].isLegacyScore) {
-                        write_byte(&db,
-                                   (it->second[i].version > 20190103
-                                        ? (it->second[i].isImportedLegacyScore ? hackIsImportedLegacyScoreFlag : 0)
-                                        : 0));  // gamemode (hardcoded atm) // NOTE: abused as isImportedLegacyScore
-                                                // flag (because I forgot to add a version cap to old builds)
-                        write_int32(&db, it->second[i].version);
-                        // HACKHACK: for explanation see hackIsImportedLegacyScoreFlag
-                        /*
-                        if (it->second[i].version > 20190103)
-                        {
-                                db.writeBool(it->second[i].isImportedLegacyScore);
-                        }
-                        */
-                        write_int64(&db, it->second[i].unixTimestamp);
-
-                        // default
-                        write_string(&db, it->second[i].playerName);
-
-                        write_short(&db, it->second[i].num300s);
-                        write_short(&db, it->second[i].num100s);
-                        write_short(&db, it->second[i].num50s);
-                        write_short(&db, it->second[i].numGekis);
-                        write_short(&db, it->second[i].numKatus);
-                        write_short(&db, it->second[i].numMisses);
+    // write header
+    write_int32(&db, dbVersion);
+    write_int32(&db, numBeatmaps);
 
-                        write_int64(&db, it->second[i].score);
-                        write_short(&db, it->second[i].comboMax);
-                        write_int32(&db, it->second[i].modsLegacy);
+    // write scores for each beatmap
+    for(auto &beatmap : m_scores) {
+        int numNonLegacyScores = 0;
+        for(int i = 0; i < beatmap.second.size(); i++) {
+            if(!beatmap.second[i].isLegacyScore) numNonLegacyScores++;
+        }
 
-                        // custom
-                        write_short(&db, it->second[i].numSliderBreaks);
-                        write_float32(&db, it->second[i].pp);
-                        write_float32(&db, it->second[i].unstableRate);
-                        write_float32(&db, it->second[i].hitErrorAvgMin);
-                        write_float32(&db, it->second[i].hitErrorAvgMax);
-                        write_float32(&db, it->second[i].starsTomTotal);
-                        write_float32(&db, it->second[i].starsTomAim);
-                        write_float32(&db, it->second[i].starsTomSpeed);
-                        write_float32(&db, it->second[i].speedMultiplier);
-                        write_float32(&db, it->second[i].CS);
-                        write_float32(&db, it->second[i].AR);
-                        write_float32(&db, it->second[i].OD);
-                        write_float32(&db, it->second[i].HP);
-
-                        if(it->second[i].version > 20180722) {
-                            write_int32(&db, it->second[i].maxPossibleCombo);
-                            write_int32(&db, it->second[i].numHitObjects);
-                            write_int32(&db, it->second[i].numCircles);
-                        }
+        if(numNonLegacyScores == 0) continue;
+
+        write_string(&db, beatmap.first.hash);  // beatmap md5 hash
+        write_int32(&db, numNonLegacyScores);   // numScores
+
+        for(auto &score : beatmap.second) {
+            if(score.isLegacyScore) continue;
+
+            uint8_t gamemode = 0;
+            if(score.version > 20190103 && score.isImportedLegacyScore) gamemode = hackIsImportedLegacyScoreFlag;
+            write_byte(&db, gamemode);
+
+            write_int32(&db, score.version);
+            write_int64(&db, score.unixTimestamp);
+
+            // default
+            write_string(&db, score.playerName);
+
+            write_short(&db, score.num300s);
+            write_short(&db, score.num100s);
+            write_short(&db, score.num50s);
+            write_short(&db, score.numGekis);
+            write_short(&db, score.numKatus);
+            write_short(&db, score.numMisses);
+
+            write_int64(&db, score.score);
+            write_short(&db, score.comboMax);
+            write_int32(&db, score.modsLegacy);
+
+            // custom
+            write_short(&db, score.numSliderBreaks);
+            write_float32(&db, score.pp);
+            write_float32(&db, score.unstableRate);
+            write_float32(&db, score.hitErrorAvgMin);
+            write_float32(&db, score.hitErrorAvgMax);
+            write_float32(&db, score.starsTomTotal);
+            write_float32(&db, score.starsTomAim);
+            write_float32(&db, score.starsTomSpeed);
+            write_float32(&db, score.speedMultiplier);
+            write_float32(&db, score.CS);
+            write_float32(&db, score.AR);
+            write_float32(&db, score.OD);
+            write_float32(&db, score.HP);
+
+            if(score.version > 20180722) {
+                write_int32(&db, score.maxPossibleCombo);
+                write_int32(&db, score.numHitObjects);
+                write_int32(&db, score.numCircles);
+            }
 
-                        write_string(&db, it->second[i].experimentalModsConVars.c_str());
-                    }
-                }
+            if(score.version >= 20240412) {
+                write_int32(&db, score.online_score_id);
+                write_string(&db, score.server.c_str());
             }
-        }
 
-        if(!save_db(&db, ("scores.db"))) {
-            debugLog("Couldn't write scores.db!\n");
+            write_string(&db, score.experimentalModsConVars.c_str());
         }
+    }
 
-        debugLog("Took %f seconds.\n", (engine->getTimeReal() - startTime));
+    if(!save_db(&db, ("scores.db"))) {
+        debugLog("Couldn't write scores.db!\n");
     }
+
+    debugLog("Took %f seconds.\n", (engine->getTimeReal() - startTime));
 }
 
 void OsuDatabase::loadCollections(std::string collectionFilePath, bool isLegacy,

+ 4 - 102
src/App/Osu/OsuDatabase.h

@@ -11,6 +11,7 @@
 #include "BanchoProtocol.h"  // Packet
 #include "OsuReplay.h"
 #include "OsuScore.h"
+#include "Score.h"
 #include "UString.h"
 #include "cbase.h"
 
@@ -46,105 +47,6 @@ class OsuDatabase {
         std::vector<std::pair<OsuDatabaseBeatmap *, std::vector<OsuDatabaseBeatmap *>>> beatmaps;
     };
 
-    struct Score {
-        bool isLegacyScore;  // used for identifying loaded osu! scores (which don't have any custom data available)
-        bool isImportedLegacyScore;  // used for identifying imported osu! scores (which were previously legacy scores,
-                                     // so they don't have any
-                                     // numSliderBreaks/unstableRate/hitErrorAvgMin/hitErrorAvgMax)
-        int version;
-        uint64_t unixTimestamp;
-
-        uint32_t player_id = 0;
-        UString playerName;
-        bool passed = false;
-        bool ragequit = false;
-        OsuScore::GRADE grade = OsuScore::GRADE::GRADE_N;
-        OsuDatabaseBeatmap *diff2;
-        uint64_t play_time_ms = 0;
-        std::vector<OsuReplay::Frame> replay;
-
-        bool has_replay = false;
-        uint64_t online_score_id = 0;
-
-        int num300s;
-        int num100s;
-        int num50s;
-        int numGekis;
-        int numKatus;
-        int numMisses;
-
-        unsigned long long score;
-        int comboMax;
-        bool perfect;
-        int modsLegacy;
-
-        // custom
-        int numSliderBreaks;
-        float pp;
-        float unstableRate;
-        float hitErrorAvgMin;
-        float hitErrorAvgMax;
-        float starsTomTotal;
-        float starsTomAim;
-        float starsTomSpeed;
-        float speedMultiplier;
-        float CS, AR, OD, HP;
-        int maxPossibleCombo;
-        int numHitObjects;
-        int numCircles;
-        std::string experimentalModsConVars;
-
-        // runtime
-        unsigned long long sortHack;
-        MD5Hash md5hash;
-
-        bool isLegacyScoreEqualToImportedLegacyScore(const OsuDatabase::Score &importedLegacyScore) const {
-            if(!isLegacyScore) return false;
-            if(!importedLegacyScore.isImportedLegacyScore) return false;
-
-            const bool isScoreValueEqual = (score == importedLegacyScore.score);
-            const bool isTimestampEqual = (unixTimestamp == importedLegacyScore.unixTimestamp);
-            const bool isComboMaxEqual = (comboMax == importedLegacyScore.comboMax);
-            const bool isModsLegacyEqual = (modsLegacy == importedLegacyScore.modsLegacy);
-            const bool isNum300sEqual = (num300s == importedLegacyScore.num300s);
-            const bool isNum100sEqual = (num100s == importedLegacyScore.num100s);
-            const bool isNum50sEqual = (num50s == importedLegacyScore.num50s);
-            const bool isNumGekisEqual = (numGekis == importedLegacyScore.numGekis);
-            const bool isNumKatusEqual = (numKatus == importedLegacyScore.numKatus);
-            const bool isNumMissesEqual = (numMisses == importedLegacyScore.numMisses);
-
-            return (isScoreValueEqual && isTimestampEqual && isComboMaxEqual && isModsLegacyEqual && isNum300sEqual &&
-                    isNum100sEqual && isNum50sEqual && isNumGekisEqual && isNumKatusEqual && isNumMissesEqual);
-        }
-
-        bool isScoreEqualToCopiedScoreIgnoringPlayerName(const OsuDatabase::Score &copiedScore) const {
-            const bool isScoreValueEqual = (score == copiedScore.score);
-            const bool isTimestampEqual = (unixTimestamp == copiedScore.unixTimestamp);
-            const bool isComboMaxEqual = (comboMax == copiedScore.comboMax);
-            const bool isModsLegacyEqual = (modsLegacy == copiedScore.modsLegacy);
-            const bool isNum300sEqual = (num300s == copiedScore.num300s);
-            const bool isNum100sEqual = (num100s == copiedScore.num100s);
-            const bool isNum50sEqual = (num50s == copiedScore.num50s);
-            const bool isNumGekisEqual = (numGekis == copiedScore.numGekis);
-            const bool isNumKatusEqual = (numKatus == copiedScore.numKatus);
-            const bool isNumMissesEqual = (numMisses == copiedScore.numMisses);
-
-            const bool isSpeedMultiplierEqual = (speedMultiplier == copiedScore.speedMultiplier);
-            const bool isCSEqual = (CS == copiedScore.CS);
-            const bool isAREqual = (AR == copiedScore.AR);
-            const bool isODEqual = (OD == copiedScore.OD);
-            const bool isHPEqual = (HP == copiedScore.HP);
-            const bool areExperimentalModsConVarsEqual =
-                (experimentalModsConVars == copiedScore.experimentalModsConVars);
-
-            return (isScoreValueEqual && isTimestampEqual && isComboMaxEqual && isModsLegacyEqual && isNum300sEqual &&
-                    isNum100sEqual && isNum50sEqual && isNumGekisEqual && isNumKatusEqual && isNumMissesEqual
-
-                    && isSpeedMultiplierEqual && isCSEqual && isAREqual && isODEqual && isHPEqual &&
-                    areExperimentalModsConVarsEqual);
-        }
-    };
-
     struct PlayerStats {
         UString name;
         float pp;
@@ -162,7 +64,7 @@ class OsuDatabase {
 
     struct SCORE_SORTING_COMPARATOR {
         virtual ~SCORE_SORTING_COMPARATOR() { ; }
-        virtual bool operator()(OsuDatabase::Score const &a, OsuDatabase::Score const &b) const = 0;
+        virtual bool operator()(Score const &a, Score const &b) const = 0;
     };
 
     struct SCORE_SORTING_METHOD {
@@ -182,7 +84,7 @@ class OsuDatabase {
 
     OsuDatabaseBeatmap *addBeatmap(std::string beatmapFolderPath);
 
-    int addScore(MD5Hash beatmapMD5Hash, OsuDatabase::Score score);
+    int addScore(MD5Hash beatmapMD5Hash, Score score);
     void deleteScore(MD5Hash beatmapMD5Hash, uint64_t scoreUnixTimestamp);
     void sortScores(MD5Hash beatmapMD5Hash);
     void forceScoreUpdateOnNextCalculatePlayerStats() { m_bDidScoresChangeForStats = true; }
@@ -231,7 +133,7 @@ class OsuDatabase {
     static ConVar *m_name_ref;
     static ConVar *m_osu_songbrowser_scores_sortingtype_ref;
 
-    void addScoreRaw(const MD5Hash &beatmapMD5Hash, const OsuDatabase::Score &score);
+    void addScoreRaw(const MD5Hash &beatmapMD5Hash, const Score &score);
 
     std::string parseLegacyCfgBeatmapDirectoryParameter();
     void scheduleLoadRaw();

+ 2 - 2
src/App/Osu/OsuHUD.cpp

@@ -31,7 +31,7 @@
 #include "OsuScoreboardSlot.h"
 #include "OsuSkin.h"
 #include "OsuSkinImage.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuUIAvatar.h"
 #include "ResourceManager.h"
 #include "Shader.h"
@@ -1506,7 +1506,7 @@ std::vector<SCORE_ENTRY> OsuHUD::getCurrentScores() {
         }
     } else {
         auto m_db = m_osu->getSongBrowser()->getDatabase();
-        std::vector<OsuDatabase::Score> *singleplayer_scores = &((*m_db->getScores())[beatmap_md5]);
+        std::vector<Score> *singleplayer_scores = &((*m_db->getScores())[beatmap_md5]);
         auto cv_sortingtype = convar->getConVarByName("osu_songbrowser_scores_sortingtype");
         bool is_online = cv_sortingtype->getString() == UString("Online Leaderboard");
         if(is_online) {

+ 2 - 2
src/App/Osu/OsuMainMenu.cpp

@@ -32,7 +32,7 @@
 #include "OsuSlider.h"
 #include "OsuSliderCurves.h"
 #include "OsuSliderRenderer.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuUIButton.h"
 #include "OsuUpdateHandler.h"
 #include "ResourceManager.h"
@@ -422,7 +422,7 @@ void OsuMainMenu::draw(Graphics *g) {
                 alpha = 1.0f - (1.0f - alpha) * (1.0f - alpha);
             }
         }
-        OsuSongBrowser2::drawSelectedBeatmapBackgroundImage(g, m_osu, alpha);
+        OsuSongBrowser::drawSelectedBeatmapBackgroundImage(g, m_osu, alpha);
     }
 
     // main button stuff

+ 1 - 1
src/App/Osu/OsuModSelector.cpp

@@ -34,7 +34,7 @@
 #include "OsuRoom.h"
 #include "OsuSkin.h"
 #include "OsuSkinImage.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuTooltipOverlay.h"
 #include "OsuUIButton.h"
 #include "OsuUICheckbox.h"

+ 12 - 12
src/App/Osu/OsuRankingScreen.cpp

@@ -29,7 +29,7 @@
 #include "OsuScore.h"
 #include "OsuSkin.h"
 #include "OsuSkinImage.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuTooltipOverlay.h"
 #include "OsuUIRankingScreenInfoLabel.h"
 #include "OsuUIRankingScreenRankingPanel.h"
@@ -207,7 +207,7 @@ OsuRankingScreen::OsuRankingScreen(Osu *osu) : OsuScreenBackable(osu) {
     addBaseUIElement(m_rankingScrollDownInfoButton);
     m_fRankingScrollDownInfoButtonAlphaAnim = 1.0f;
 
-    setGrade(OsuScore::GRADE::GRADE_D);
+    setGrade(Score::Grade::D);
     setIndex(0);  // TEMP
 
     m_fUnstableRate = 0.0f;
@@ -251,7 +251,7 @@ void OsuRankingScreen::draw(Graphics *g) {
     if(!m_bVisible) return;
 
     // draw background image
-    if(osu_draw_rankingscreen_background_image.getBool()) OsuSongBrowser2::drawSelectedBeatmapBackgroundImage(g, m_osu);
+    if(osu_draw_rankingscreen_background_image.getBool()) OsuSongBrowser::drawSelectedBeatmapBackgroundImage(g, m_osu);
 
     m_rankings->draw(g);
 
@@ -499,7 +499,7 @@ void OsuRankingScreen::setScore(OsuScore *score) {
     m_bIsUnranked = score->isUnranked();
 }
 
-void OsuRankingScreen::setScore(OsuDatabase::Score score, UString dateTime) {
+void OsuRankingScreen::setScore(Score score, UString dateTime) {
     m_bIsLegacyScore = score.isLegacyScore;
     m_bIsImportedLegacyScore = score.isImportedLegacyScore;
     m_bIsUnranked = false;
@@ -649,36 +649,36 @@ void OsuRankingScreen::onBack() {
 
 void OsuRankingScreen::onScrollDownClicked() { m_rankings->scrollToBottom(); }
 
-void OsuRankingScreen::setGrade(OsuScore::GRADE grade) {
+void OsuRankingScreen::setGrade(Score::Grade grade) {
     m_grade = grade;
 
     Vector2 hardcodedOsuRankingGradeImageSize = Vector2(369, 422);
     switch(grade) {
-        case OsuScore::GRADE::GRADE_XH:
+        case Score::Grade::XH:
             hardcodedOsuRankingGradeImageSize *= (m_osu->getSkin()->isRankingXH2x() ? 2.0f : 1.0f);
             m_rankingGrade->setImage(m_osu->getSkin()->getRankingXH());
             break;
-        case OsuScore::GRADE::GRADE_SH:
+        case Score::Grade::SH:
             hardcodedOsuRankingGradeImageSize *= (m_osu->getSkin()->isRankingSH2x() ? 2.0f : 1.0f);
             m_rankingGrade->setImage(m_osu->getSkin()->getRankingSH());
             break;
-        case OsuScore::GRADE::GRADE_X:
+        case Score::Grade::X:
             hardcodedOsuRankingGradeImageSize *= (m_osu->getSkin()->isRankingX2x() ? 2.0f : 1.0f);
             m_rankingGrade->setImage(m_osu->getSkin()->getRankingX());
             break;
-        case OsuScore::GRADE::GRADE_S:
+        case Score::Grade::S:
             hardcodedOsuRankingGradeImageSize *= (m_osu->getSkin()->isRankingS2x() ? 2.0f : 1.0f);
             m_rankingGrade->setImage(m_osu->getSkin()->getRankingS());
             break;
-        case OsuScore::GRADE::GRADE_A:
+        case Score::Grade::A:
             hardcodedOsuRankingGradeImageSize *= (m_osu->getSkin()->isRankingA2x() ? 2.0f : 1.0f);
             m_rankingGrade->setImage(m_osu->getSkin()->getRankingA());
             break;
-        case OsuScore::GRADE::GRADE_B:
+        case Score::Grade::B:
             hardcodedOsuRankingGradeImageSize *= (m_osu->getSkin()->isRankingB2x() ? 2.0f : 1.0f);
             m_rankingGrade->setImage(m_osu->getSkin()->getRankingB());
             break;
-        case OsuScore::GRADE::GRADE_C:
+        case Score::Grade::C:
             hardcodedOsuRankingGradeImageSize *= (m_osu->getSkin()->isRankingC2x() ? 2.0f : 1.0f);
             m_rankingGrade->setImage(m_osu->getSkin()->getRankingC());
             break;

+ 3 - 3
src/App/Osu/OsuRankingScreen.h

@@ -40,7 +40,7 @@ class OsuRankingScreen : public OsuScreenBackable {
     virtual CBaseUIContainer *setVisible(bool visible);
 
     void setScore(OsuScore *score);
-    void setScore(OsuDatabase::Score score, UString dateTime);
+    void setScore(Score score, UString dateTime);
     void setBeatmapInfo(OsuBeatmap *beatmap, OsuDatabaseBeatmap *diff2);
 
    private:
@@ -51,7 +51,7 @@ class OsuRankingScreen : public OsuScreenBackable {
 
     void onScrollDownClicked();
 
-    void setGrade(OsuScore::GRADE grade);
+    void setGrade(Score::Grade grade);
     void setIndex(int index);
 
     UString getPPString();
@@ -72,7 +72,7 @@ class OsuRankingScreen : public OsuScreenBackable {
     OsuRankingScreenScrollDownInfoButton *m_rankingScrollDownInfoButton;
     float m_fRankingScrollDownInfoButtonAlphaAnim;
 
-    OsuScore::GRADE m_grade;
+    Score::Grade m_grade;
     float m_fUnstableRate;
     float m_fHitErrorAvgMin;
     float m_fHitErrorAvgMax;

+ 153 - 7
src/App/Osu/OsuReplay.cpp

@@ -1,16 +1,16 @@
-//================ Copyright (c) 2016, PG, All rights reserved. =================//
-//
-// Purpose:		replay handler & parser
-//
-// $NoKeywords: $osr
-//===============================================================================//
-
 #include "OsuReplay.h"
 
 #include <lzma.h>
 
+#include "Bancho.h"
 #include "BanchoProtocol.h"
 #include "Engine.h"
+#include "Osu.h"
+#include "OsuBeatmap.h"
+#include "OsuDatabase.h"
+#include "OsuNotificationOverlay.h"
+#include "OsuSongBrowser.h"
+#include "Score.h"
 
 OsuReplay::BEATMAP_VALUES OsuReplay::getBeatmapValuesForModsLegacy(int modsLegacy, float legacyAR, float legacyCS,
                                                                    float legacyOD, float legacyHP) {
@@ -93,6 +93,56 @@ end:
     return replay_frames;
 }
 
+void OsuReplay::compress_frames(const std::vector<OsuReplay::Frame>& frames, uint8_t** compressed,
+                                size_t* s_compressed) {
+    lzma_stream stream = LZMA_STREAM_INIT;
+    lzma_options_lzma options;
+    lzma_lzma_preset(&options, LZMA_PRESET_DEFAULT);
+    lzma_ret ret = lzma_alone_encoder(&stream, &options);
+    if(ret != LZMA_OK) {
+        debugLog("Failed to initialize lzma encoder: error %d\n", ret);
+        *compressed = NULL;
+        *s_compressed = 0;
+        return;
+    }
+
+    std::string replay_string;
+    for(auto frame : frames) {
+        auto frame_str = UString::format("%ld|%.4f|%.4f|%hhu,", frame.milliseconds_since_last_frame, frame.x, frame.y,
+                                         frame.key_flags);
+        replay_string.append(frame_str.toUtf8(), frame_str.lengthUtf8());
+    }
+
+    // osu!stable doesn't consider a replay valid unless it ends with this
+    replay_string.append("-12345|0.0000|0.0000|0,");
+
+    *s_compressed = replay_string.length();
+    *compressed = (uint8_t*)malloc(*s_compressed);
+
+    stream.avail_in = replay_string.length();
+    stream.next_in = (const uint8_t*)replay_string.c_str();
+    stream.avail_out = *s_compressed;
+    stream.next_out = *compressed;
+    do {
+        ret = lzma_code(&stream, LZMA_FINISH);
+        if(ret == LZMA_OK) {
+            *s_compressed *= 2;
+            *compressed = (uint8_t*)realloc(*compressed, *s_compressed);
+            stream.avail_out = *s_compressed - stream.total_out;
+            stream.next_out = *compressed + stream.total_out;
+        } else if(ret != LZMA_STREAM_END) {
+            debugLog("Error while compressing replay: error %d\n", ret);
+            *compressed = NULL;
+            *s_compressed = 0;
+            lzma_end(&stream);
+            return;
+        }
+    } while(ret != LZMA_STREAM_END);
+
+    *s_compressed = stream.total_out;
+    lzma_end(&stream);
+}
+
 OsuReplay::Info OsuReplay::from_bytes(uint8_t* data, int s_data) {
     OsuReplay::Info info;
 
@@ -132,3 +182,99 @@ OsuReplay::Info OsuReplay::from_bytes(uint8_t* data, int s_data) {
 
     return info;
 }
+
+bool OsuReplay::load_from_disk(Score* score) {
+    if(score->legacyReplayTimestamp > 0) {
+        auto osu_folder = convar->getConVarByName("osu_folder")->getString();
+        auto path = UString::format("%s/Data/r/%s-%llu.osr", osu_folder.toUtf8(), score->md5hash.hash,
+                                    score->legacyReplayTimestamp);
+
+        FILE* replay_file = fopen(path.toUtf8(), "rb");
+        if(replay_file == NULL) return false;
+
+        fseek(replay_file, 0, SEEK_END);
+        size_t s_full_replay = ftell(replay_file);
+        rewind(replay_file);
+
+        uint8_t* full_replay = new uint8_t[s_full_replay];
+        fread(full_replay, s_full_replay, 1, replay_file);
+        fclose(replay_file);
+        auto info = OsuReplay::from_bytes(full_replay, s_full_replay);
+        score->replay = info.frames;
+        delete[] full_replay;
+    } else {
+        auto path = UString::format(MCENGINE_DATA_DIR "replays/%s/%llu.replay.lzma", score->server.c_str(),
+                                    score->unixTimestamp);
+
+        FILE* replay_file = fopen(path.toUtf8(), "rb");
+        if(replay_file == NULL) return false;
+
+        fseek(replay_file, 0, SEEK_END);
+        size_t s_compressed_replay = ftell(replay_file);
+        rewind(replay_file);
+
+        uint8_t* compressed_replay = new uint8_t[s_compressed_replay];
+        fread(compressed_replay, s_compressed_replay, 1, replay_file);
+        fclose(replay_file);
+        score->replay = OsuReplay::get_frames(compressed_replay, s_compressed_replay);
+        delete[] compressed_replay;
+    }
+
+    auto& map_scores = (*(bancho.osu->getSongBrowser()->getDatabase()->getScores()))[score->md5hash];
+    for(auto& db_score : map_scores) {
+        if(db_score.unixTimestamp != score->unixTimestamp) continue;
+        if(&db_score != score) {
+            db_score.replay = score->replay;
+        }
+
+        break;
+    }
+
+    return true;
+}
+
+void OsuReplay::load_and_watch(Score score) {
+    // Check if replay is loaded
+    if(score.replay.empty()) {
+        if(!load_from_disk(&score)) {
+            if(strcmp(score.server.c_str(), bancho.endpoint.toUtf8()) != 0) {
+                auto msg = UString::format("Please connect to %s to view this replay!", score.server.c_str());
+                bancho.osu->m_notificationOverlay->addNotification(msg);
+            }
+
+            Score* score_cpy = (Score*)malloc(sizeof(Score));
+            *score_cpy = score;
+
+            APIRequest request;
+            request.type = GET_REPLAY;
+            request.path = UString::format("/web/osu-getreplay.php?u=%s&h=%s&m=0&c=%d", bancho.username.toUtf8(),
+                                           bancho.pw_md5.toUtf8(), score.online_score_id);
+            request.mime = NULL;
+            request.extra = (uint8_t*)score_cpy;
+            send_api_request(request);
+
+            bancho.osu->m_notificationOverlay->addNotification("Downloading replay...");
+            return;
+        }
+    }
+
+    // We tried loading from memory, we tried loading from file, we tried loading from server... RIP
+    if(score.replay.empty()) {
+        bancho.osu->m_notificationOverlay->addNotification("Failed to load replay");
+        return;
+    }
+
+    bancho.osu->replay_info.diff2_md5 = score.md5hash.hash;
+    bancho.osu->replay_info.mod_flags = score.modsLegacy;
+    bancho.osu->replay_info.username = score.playerName;
+    bancho.osu->replay_info.player_id = score.player_id;
+
+    auto beatmap = bancho.osu->getSongBrowser()->getDatabase()->getBeatmapDifficulty(score.md5hash.hash);
+    if(beatmap == nullptr) {
+        // XXX: Auto-download beatmap
+        bancho.osu->m_notificationOverlay->addNotification("Missing beatmap for this replay");
+    } else {
+        bancho.osu->getSongBrowser()->onDifficultySelected(beatmap, false);
+        bancho.osu->getSelectedBeatmap()->watch(score);
+    }
+}

+ 5 - 0
src/App/Osu/OsuReplay.h

@@ -2,6 +2,8 @@
 #include "ModFlags.h"
 #include "cbase.h"
 
+struct Score;
+
 namespace OsuReplay {
 struct Frame {
     int64_t cur_music_pos;
@@ -59,5 +61,8 @@ BEATMAP_VALUES getBeatmapValuesForModsLegacy(int modsLegacy, float legacyAR, flo
 
 Info from_bytes(uint8_t* data, int s_data);
 std::vector<Frame> get_frames(uint8_t* replay_data, int32_t replay_size);
+void compress_frames(const std::vector<Frame>& frames, uint8_t** compressed, size_t* s_compressed);
+bool load_from_disk(Score* score);
+void load_and_watch(Score score);
 
 }  // namespace OsuReplay

+ 1 - 1
src/App/Osu/OsuRichPresence.cpp

@@ -20,7 +20,7 @@
 #include "OsuModSelector.h"
 #include "OsuRoom.h"
 #include "OsuScore.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 
 ConVar osu_rich_presence("osu_rich_presence", true, FCVAR_NONE, OsuRichPresence::onRichPresenceChange);
 ConVar osu_rich_presence_dynamic_windowtitle(

+ 2 - 2
src/App/Osu/OsuRoom.cpp

@@ -28,7 +28,7 @@
 #include "OsuRichPresence.h"
 #include "OsuSkin.h"
 #include "OsuSkinImage.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuUIAvatar.h"
 #include "OsuUIButton.h"
 #include "OsuUICheckbox.h"
@@ -197,7 +197,7 @@ void OsuRoom::draw(Graphics *g) {
     if(!m_bVisible) return;
 
     // XXX: Add convar for toggling room backgrounds
-    OsuSongBrowser2::drawSelectedBeatmapBackgroundImage(g, m_osu, 1.0);
+    OsuSongBrowser::drawSelectedBeatmapBackgroundImage(g, m_osu, 1.0);
     OsuScreen::draw(g);
 
     // Update avatar visibility status

+ 15 - 15
src/App/Osu/OsuScore.cpp

@@ -65,7 +65,7 @@ void OsuScore::reset() {
     m_hitresults = std::vector<HIT>();
     m_hitdeltas = std::vector<int>();
 
-    m_grade = OsuScore::GRADE::GRADE_N;
+    m_grade = Score::Grade::N;
 
     m_fStarsTomTotal = 0.0f;
     m_fStarsTomAim = 0.0f;
@@ -217,14 +217,14 @@ void OsuScore::addHitResult(OsuBeatmap *beatmap, OsuHitObject *hitObject, HIT hi
     }
 
     // recalculate grade
-    m_grade = OsuScore::GRADE::GRADE_D;
-    if(percent300s > 0.6f) m_grade = OsuScore::GRADE::GRADE_C;
-    if((percent300s > 0.7f && m_iNumMisses == 0) || (percent300s > 0.8f)) m_grade = OsuScore::GRADE::GRADE_B;
-    if((percent300s > 0.8f && m_iNumMisses == 0) || (percent300s > 0.9f)) m_grade = OsuScore::GRADE::GRADE_A;
+    m_grade = Score::Grade::D;
+    if(percent300s > 0.6f) m_grade = Score::Grade::C;
+    if((percent300s > 0.7f && m_iNumMisses == 0) || (percent300s > 0.8f)) m_grade = Score::Grade::B;
+    if((percent300s > 0.8f && m_iNumMisses == 0) || (percent300s > 0.9f)) m_grade = Score::Grade::A;
     if(percent300s > 0.9f && percent50s <= 0.01f && m_iNumMisses == 0)
-        m_grade = m_osu->getModHD() || m_osu->getModFlashlight() ? OsuScore::GRADE::GRADE_SH : OsuScore::GRADE::GRADE_S;
+        m_grade = m_osu->getModHD() || m_osu->getModFlashlight() ? Score::Grade::SH : Score::Grade::S;
     if(m_iNumMisses == 0 && m_iNum50s == 0 && m_iNum100s == 0)
-        m_grade = m_osu->getModHD() || m_osu->getModFlashlight() ? OsuScore::GRADE::GRADE_XH : OsuScore::GRADE::GRADE_X;
+        m_grade = m_osu->getModHD() || m_osu->getModFlashlight() ? Score::Grade::XH : Score::Grade::X;
 
     // recalculate unstable rate
     float averageDelta = 0.0f;
@@ -622,8 +622,8 @@ float OsuScore::calculateAccuracy(int num300s, int num100s, int num50s, int numM
     return 0.0f;
 }
 
-OsuScore::GRADE OsuScore::calculateGrade(int num300s, int num100s, int num50s, int numMisses, bool modHidden,
-                                         bool modFlashlight) {
+Score::Grade OsuScore::calculateGrade(int num300s, int num100s, int num50s, int numMisses, bool modHidden,
+                                      bool modFlashlight) {
     const float totalNumHits = numMisses + num50s + num100s + num300s;
 
     float percent300s = 0.0f;
@@ -633,14 +633,14 @@ OsuScore::GRADE OsuScore::calculateGrade(int num300s, int num100s, int num50s, i
         percent50s = num50s / totalNumHits;
     }
 
-    GRADE grade = OsuScore::GRADE::GRADE_D;
-    if(percent300s > 0.6f) grade = OsuScore::GRADE::GRADE_C;
-    if((percent300s > 0.7f && numMisses == 0) || (percent300s > 0.8f)) grade = OsuScore::GRADE::GRADE_B;
-    if((percent300s > 0.8f && numMisses == 0) || (percent300s > 0.9f)) grade = OsuScore::GRADE::GRADE_A;
+    Score::Grade grade = Score::Grade::D;
+    if(percent300s > 0.6f) grade = Score::Grade::C;
+    if((percent300s > 0.7f && numMisses == 0) || (percent300s > 0.8f)) grade = Score::Grade::B;
+    if((percent300s > 0.8f && numMisses == 0) || (percent300s > 0.9f)) grade = Score::Grade::A;
     if(percent300s > 0.9f && percent50s <= 0.01f && numMisses == 0)
-        grade = ((modHidden || modFlashlight) ? OsuScore::GRADE::GRADE_SH : OsuScore::GRADE::GRADE_S);
+        grade = ((modHidden || modFlashlight) ? Score::Grade::SH : Score::Grade::S);
     if(numMisses == 0 && num50s == 0 && num100s == 0)
-        grade = ((modHidden || modFlashlight) ? OsuScore::GRADE::GRADE_XH : OsuScore::GRADE::GRADE_X);
+        grade = ((modHidden || modFlashlight) ? Score::Grade::XH : Score::Grade::X);
 
     return grade;
 }

+ 6 - 19
src/App/Osu/OsuScore.h

@@ -8,7 +8,7 @@
 #ifndef OSUSCORE_H
 #define OSUSCORE_H
 
-#include "cbase.h"
+#include "Score.h"
 
 class ConVar;
 
@@ -18,7 +18,7 @@ class OsuHitObject;
 
 class OsuScore {
    public:
-    static constexpr const int VERSION = 20220902;
+    static constexpr const int VERSION = 20240412;
 
     enum class HIT {
         // score
@@ -40,22 +40,9 @@ class OsuScore {
         HIT_SPINNERBONUS
     };
 
-    enum class GRADE {
-        GRADE_XH,
-        GRADE_SH,
-        GRADE_X,
-        GRADE_S,
-        GRADE_A,
-        GRADE_B,
-        GRADE_C,
-        GRADE_D,
-        GRADE_F,
-        GRADE_N  // means "no grade"
-    };
-
     static float calculateAccuracy(int num300s, int num100s, int num50s, int numMisses);
-    static GRADE calculateGrade(int num300s, int num100s, int num50s, int numMisses, bool modHidden,
-                                bool modFlashlight);
+    static Score::Grade calculateGrade(int num300s, int num100s, int num50s, int numMisses, bool modHidden,
+                                       bool modFlashlight);
 
    public:
     OsuScore(Osu *osu);
@@ -89,7 +76,7 @@ class OsuScore {
     inline int getIndex() const { return m_iIndex; }
 
     unsigned long long getScore();
-    inline GRADE getGrade() const { return m_grade; }
+    inline Score::Grade getGrade() const { return m_grade; }
     inline int getCombo() const { return m_iCombo; }
     inline int getComboMax() const { return m_iComboMax; }
     inline int getComboFull() const { return m_iComboFull; }
@@ -134,7 +121,7 @@ class OsuScore {
     std::vector<HIT> m_hitresults;
     std::vector<int> m_hitdeltas;
 
-    GRADE m_grade;
+    Score::Grade m_grade;
 
     float m_fStarsTomTotal;
     float m_fStarsTomAim;

+ 126 - 131
src/App/Osu/OsuSongBrowser2.cpp → src/App/Osu/OsuSongBrowser.cpp

@@ -5,8 +5,6 @@
 // $NoKeywords: $osusb
 //===============================================================================//
 
-#include "OsuSongBrowser2.h"
-
 #include "AnimationHandler.h"
 #include "Bancho.h"
 #include "BanchoLeaderboard.h"
@@ -37,6 +35,7 @@
 #include "OsuRoom.h"
 #include "OsuSkin.h"
 #include "OsuSkinImage.h"
+#include "OsuSongBrowser.h"
 #include "OsuUIBackButton.h"
 #include "OsuUIContextMenu.h"
 #include "OsuUISearchOverlay.h"
@@ -130,11 +129,11 @@ class OsuSongBrowserBackgroundSearchMatcher : public Resource {
             if(children.size() > 0) {
                 for(size_t c = 0; c < children.size(); c++) {
                     children[c]->setIsSearchMatch(
-                        OsuSongBrowser2::searchMatcher(children[c]->getDatabaseBeatmap(), searchStringTokens));
+                        OsuSongBrowser::searchMatcher(children[c]->getDatabaseBeatmap(), searchStringTokens));
                 }
             } else
                 m_songButtons[i]->setIsSearchMatch(
-                    OsuSongBrowser2::searchMatcher(m_songButtons[i]->getDatabaseBeatmap(), searchStringTokens));
+                    OsuSongBrowser::searchMatcher(m_songButtons[i]->getDatabaseBeatmap(), searchStringTokens));
 
             // cancellation point
             if(m_bDead.load()) break;
@@ -253,7 +252,7 @@ class OsuUISongBrowserNoRecordsSetElement : public CBaseUILabel {
     UString m_sIconString;
 };
 
-bool OsuSongBrowser2::SortByArtist::operator()(OsuUISongBrowserButton const *a, OsuUISongBrowserButton const *b) const {
+bool OsuSongBrowser::SortByArtist::operator()(OsuUISongBrowserButton const *a, OsuUISongBrowserButton const *b) const {
     if(a->getDatabaseBeatmap() == NULL || b->getDatabaseBeatmap() == NULL) return a->getSortHack() < b->getSortHack();
 
     // strict weak ordering!
@@ -263,7 +262,7 @@ bool OsuSongBrowser2::SortByArtist::operator()(OsuUISongBrowserButton const *a,
     return a->getDatabaseBeatmap()->getArtist().lessThanIgnoreCase(b->getDatabaseBeatmap()->getArtist());
 }
 
-bool OsuSongBrowser2::SortByBPM::operator()(OsuUISongBrowserButton const *a, OsuUISongBrowserButton const *b) const {
+bool OsuSongBrowser::SortByBPM::operator()(OsuUISongBrowserButton const *a, OsuUISongBrowserButton const *b) const {
     if(a->getDatabaseBeatmap() == NULL || b->getDatabaseBeatmap() == NULL) return a->getSortHack() < b->getSortHack();
 
     int bpm1 = a->getDatabaseBeatmap()->getMostCommonBPM();
@@ -284,8 +283,7 @@ bool OsuSongBrowser2::SortByBPM::operator()(OsuUISongBrowserButton const *a, Osu
     return bpm1 < bpm2;
 }
 
-bool OsuSongBrowser2::SortByCreator::operator()(OsuUISongBrowserButton const *a,
-                                                OsuUISongBrowserButton const *b) const {
+bool OsuSongBrowser::SortByCreator::operator()(OsuUISongBrowserButton const *a, OsuUISongBrowserButton const *b) const {
     if(a->getDatabaseBeatmap() == NULL || b->getDatabaseBeatmap() == NULL) return a->getSortHack() < b->getSortHack();
 
     // strict weak ordering!
@@ -295,8 +293,8 @@ bool OsuSongBrowser2::SortByCreator::operator()(OsuUISongBrowserButton const *a,
     return a->getDatabaseBeatmap()->getCreator().lessThanIgnoreCase(b->getDatabaseBeatmap()->getCreator());
 }
 
-bool OsuSongBrowser2::SortByDateAdded::operator()(OsuUISongBrowserButton const *a,
-                                                  OsuUISongBrowserButton const *b) const {
+bool OsuSongBrowser::SortByDateAdded::operator()(OsuUISongBrowserButton const *a,
+                                                 OsuUISongBrowserButton const *b) const {
     if(a->getDatabaseBeatmap() == NULL || b->getDatabaseBeatmap() == NULL) return a->getSortHack() < b->getSortHack();
 
     long long time1 = a->getDatabaseBeatmap()->getLastModificationTime();
@@ -317,8 +315,8 @@ bool OsuSongBrowser2::SortByDateAdded::operator()(OsuUISongBrowserButton const *
     return time1 > time2;
 }
 
-bool OsuSongBrowser2::SortByDifficulty::operator()(OsuUISongBrowserButton const *a,
-                                                   OsuUISongBrowserButton const *b) const {
+bool OsuSongBrowser::SortByDifficulty::operator()(OsuUISongBrowserButton const *a,
+                                                  OsuUISongBrowserButton const *b) const {
     if(a->getDatabaseBeatmap() == NULL || b->getDatabaseBeatmap() == NULL) return a->getSortHack() < b->getSortHack();
 
     float diff1 = (a->getDatabaseBeatmap()->getAR() + 1) * (a->getDatabaseBeatmap()->getCS() + 1) *
@@ -362,7 +360,7 @@ bool OsuSongBrowser2::SortByDifficulty::operator()(OsuUISongBrowserButton const
     }
 }
 
-bool OsuSongBrowser2::SortByLength::operator()(OsuUISongBrowserButton const *a, OsuUISongBrowserButton const *b) const {
+bool OsuSongBrowser::SortByLength::operator()(OsuUISongBrowserButton const *a, OsuUISongBrowserButton const *b) const {
     if(a->getDatabaseBeatmap() == NULL || b->getDatabaseBeatmap() == NULL) return a->getSortHack() < b->getSortHack();
 
     unsigned long length1 = a->getDatabaseBeatmap()->getLengthMS();
@@ -383,7 +381,7 @@ bool OsuSongBrowser2::SortByLength::operator()(OsuUISongBrowserButton const *a,
     return length1 < length2;
 }
 
-bool OsuSongBrowser2::SortByTitle::operator()(OsuUISongBrowserButton const *a, OsuUISongBrowserButton const *b) const {
+bool OsuSongBrowser::SortByTitle::operator()(OsuUISongBrowserButton const *a, OsuUISongBrowserButton const *b) const {
     if(a->getDatabaseBeatmap() == NULL || b->getDatabaseBeatmap() == NULL) return a->getSortHack() < b->getSortHack();
 
     // strict weak ordering!
@@ -393,7 +391,7 @@ bool OsuSongBrowser2::SortByTitle::operator()(OsuUISongBrowserButton const *a, O
     return a->getDatabaseBeatmap()->getTitle().lessThanIgnoreCase(b->getDatabaseBeatmap()->getTitle());
 }
 
-OsuSongBrowser2::OsuSongBrowser2(Osu *osu) : OsuScreenBackable(osu) {
+OsuSongBrowser::OsuSongBrowser(Osu *osu) : OsuScreenBackable(osu) {
     m_osu = osu;
 
     // random selection algorithm init
@@ -456,7 +454,7 @@ OsuSongBrowser2::OsuSongBrowser2(Osu *osu) : OsuScreenBackable(osu) {
     m_osu_mod_fposu_ref = convar->getConVarByName("osu_mod_fposu");
 
     // convar callbacks
-    osu_gamemode.setCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onModeChange));
+    osu_gamemode.setCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onModeChange));
 
     // vars
     m_bSongBrowserRightClickScrollCheck = false;
@@ -490,9 +488,9 @@ OsuSongBrowser2::OsuSongBrowser2(Osu *osu) : OsuScreenBackable(osu) {
     }
 
     m_scoreSortButton = addTopBarLeftTabButton("Sort By Score");
-    m_scoreSortButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onSortScoresClicked));
+    m_scoreSortButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onSortScoresClicked));
     m_webButton = addTopBarLeftButton("Web");
-    m_webButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onWebClicked));
+    m_webButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onWebClicked));
 
     // build topbar right
     m_topbarRight = new CBaseUIContainer(0, 0, 0, 0, "");
@@ -507,21 +505,21 @@ OsuSongBrowser2::OsuSongBrowser2(Osu *osu) : OsuScreenBackable(osu) {
         m_topbarRight->addBaseUIElement(m_groupLabel);
     }
     m_groupButton = addTopBarRightGroupButton("No Grouping");
-    m_groupButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onGroupClicked));
+    m_groupButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onGroupClicked));
 
     {
         // "hardcoded" grouping tabs
         m_collectionsButton = addTopBarRightTabButton("Collections");
         m_collectionsButton->setClickCallback(
-            fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onGroupTabButtonClicked));
+            fastdelegate::MakeDelegate(this, &OsuSongBrowser::onGroupTabButtonClicked));
         m_artistButton = addTopBarRightTabButton("By Artist");
-        m_artistButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onGroupTabButtonClicked));
+        m_artistButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onGroupTabButtonClicked));
         m_difficultiesButton = addTopBarRightTabButton("By Difficulty");
         m_difficultiesButton->setClickCallback(
-            fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onGroupTabButtonClicked));
+            fastdelegate::MakeDelegate(this, &OsuSongBrowser::onGroupTabButtonClicked));
         m_noGroupingButton = addTopBarRightTabButton("No Grouping");
         m_noGroupingButton->setClickCallback(
-            fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onGroupTabButtonClicked));
+            fastdelegate::MakeDelegate(this, &OsuSongBrowser::onGroupTabButtonClicked));
         m_noGroupingButton->setTextBrightColor(COLOR(255, 0, 255, 0));
     }
 
@@ -537,7 +535,7 @@ OsuSongBrowser2::OsuSongBrowser2(Osu *osu) : OsuScreenBackable(osu) {
         m_topbarRight->addBaseUIElement(m_sortLabel);
     }
     m_sortButton = addTopBarRightSortButton("By Date Added");
-    m_sortButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onSortClicked));
+    m_sortButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onSortClicked));
 
     // context menu
     m_contextMenu = new OsuUIContextMenu(m_osu, 50, 50, 150, 0, "");
@@ -548,20 +546,20 @@ OsuSongBrowser2::OsuSongBrowser2(Osu *osu) : OsuScreenBackable(osu) {
 
     addBottombarNavButton([this]() -> OsuSkinImage * { return m_osu->getSkin()->getSelectionMode(); },
                           [this]() -> OsuSkinImage * { return m_osu->getSkin()->getSelectionModeOver(); })
-        ->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onSelectionMode));
+        ->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onSelectionMode));
     addBottombarNavButton([this]() -> OsuSkinImage * { return m_osu->getSkin()->getSelectionMods(); },
                           [this]() -> OsuSkinImage * { return m_osu->getSkin()->getSelectionModsOver(); })
-        ->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onSelectionMods));
+        ->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onSelectionMods));
     addBottombarNavButton([this]() -> OsuSkinImage * { return m_osu->getSkin()->getSelectionRandom(); },
                           [this]() -> OsuSkinImage * { return m_osu->getSkin()->getSelectionRandomOver(); })
-        ->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onSelectionRandom));
+        ->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onSelectionRandom));
     addBottombarNavButton([this]() -> OsuSkinImage * { return m_osu->getSkin()->getSelectionOptions(); },
                           [this]() -> OsuSkinImage * { return m_osu->getSkin()->getSelectionOptionsOver(); })
-        ->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onSelectionOptions));
+        ->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onSelectionOptions));
 
     m_userButton = new OsuUISongBrowserUserButton(m_osu);
     m_userButton->addTooltipLine("Click to change [User] or view/edit [Top Ranks]");
-    m_userButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onUserButtonClicked));
+    m_userButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onUserButtonClicked));
     m_userButton->setText(m_name_ref->getString());
     m_bottombar->addBaseUIElement(m_userButton);
 
@@ -633,7 +631,7 @@ OsuSongBrowser2::OsuSongBrowser2(Osu *osu) : OsuScreenBackable(osu) {
     updateLayout();
 }
 
-OsuSongBrowser2::~OsuSongBrowser2() {
+OsuSongBrowser::~OsuSongBrowser() {
     checkHandleKillBackgroundStarCalculator();
     checkHandleKillDynamicStarCalculator(false);
     checkHandleKillBackgroundSearchMatcher();
@@ -692,7 +690,7 @@ OsuSongBrowser2::~OsuSongBrowser2() {
     SAFE_DELETE(m_db);
 }
 
-void OsuSongBrowser2::draw(Graphics *g) {
+void OsuSongBrowser::draw(Graphics *g) {
     if(!m_bVisible) return;
 
     // draw background
@@ -1004,7 +1002,7 @@ void OsuSongBrowser2::draw(Graphics *g) {
     }
 }
 
-void OsuSongBrowser2::drawSelectedBeatmapBackgroundImage(Graphics *g, Osu *osu, float alpha) {
+void OsuSongBrowser::drawSelectedBeatmapBackgroundImage(Graphics *g, Osu *osu, float alpha) {
     if(osu->getSelectedBeatmap() != NULL && osu->getSelectedBeatmap()->getSelectedDifficulty2() != NULL) {
         Image *backgroundImage = osu->getBackgroundImageHandler()->getLoadBackgroundImage(
             osu->getSelectedBeatmap()->getSelectedDifficulty2());
@@ -1024,7 +1022,7 @@ void OsuSongBrowser2::drawSelectedBeatmapBackgroundImage(Graphics *g, Osu *osu,
     }
 }
 
-void OsuSongBrowser2::mouse_update(bool *propagate_clicks) {
+void OsuSongBrowser::mouse_update(bool *propagate_clicks) {
     // HACKHACK: temporarily putting this on top as to support recalc while inPlayMode() (and songbrowser is invisible)
     // for drawing strain graph in scrubbing timeline (and total stars statistics, and max total pp statistics) handle
     // background star calculation (1)
@@ -1225,7 +1223,7 @@ void OsuSongBrowser2::mouse_update(bool *propagate_clicks) {
     }
 }
 
-void OsuSongBrowser2::onKeyDown(KeyboardEvent &key) {
+void OsuSongBrowser::onKeyDown(KeyboardEvent &key) {
     OsuScreen::onKeyDown(key);  // only used for options menu
     if(!m_bVisible || key.isConsumed()) return;
 
@@ -1488,7 +1486,7 @@ void OsuSongBrowser2::onKeyDown(KeyboardEvent &key) {
     key.consume();
 }
 
-void OsuSongBrowser2::onKeyUp(KeyboardEvent &key) {
+void OsuSongBrowser::onKeyUp(KeyboardEvent &key) {
     // context menu
     m_contextMenu->onKeyUp(key);
     if(key.isConsumed()) return;
@@ -1502,7 +1500,7 @@ void OsuSongBrowser2::onKeyUp(KeyboardEvent &key) {
     if(key == KEY_F3) m_bF3Pressed = false;
 }
 
-void OsuSongBrowser2::onChar(KeyboardEvent &e) {
+void OsuSongBrowser::onChar(KeyboardEvent &e) {
     // context menu
     m_contextMenu->onChar(e);
     if(e.isConsumed()) return;
@@ -1521,11 +1519,9 @@ void OsuSongBrowser2::onChar(KeyboardEvent &e) {
     scheduleSearchUpdate();
 }
 
-void OsuSongBrowser2::onResolutionChange(Vector2 newResolution) {
-    OsuScreenBackable::onResolutionChange(newResolution);
-}
+void OsuSongBrowser::onResolutionChange(Vector2 newResolution) { OsuScreenBackable::onResolutionChange(newResolution); }
 
-CBaseUIContainer *OsuSongBrowser2::setVisible(bool visible) {
+CBaseUIContainer *OsuSongBrowser::setVisible(bool visible) {
     m_bVisible = visible;
     m_bShiftPressed = false;  // seems to get stuck sometimes otherwise
 
@@ -1569,7 +1565,7 @@ CBaseUIContainer *OsuSongBrowser2::setVisible(bool visible) {
     return this;
 }
 
-void OsuSongBrowser2::selectSelectedBeatmapSongButton() {
+void OsuSongBrowser::selectSelectedBeatmapSongButton() {
     if(m_selectedBeatmap == nullptr) return;
 
     const std::vector<CBaseUIElement *> &elements = m_songBrowser->getContainer()->getElements();
@@ -1593,7 +1589,7 @@ void OsuSongBrowser2::selectSelectedBeatmapSongButton() {
     debugLog("No song button found for currently selected beatmap...\n");
 }
 
-void OsuSongBrowser2::onPlayEnd(bool quit) {
+void OsuSongBrowser::onPlayEnd(bool quit) {
     m_bHasSelectedAndIsPlaying = false;
 
     // update score displays
@@ -1610,7 +1606,7 @@ void OsuSongBrowser2::onPlayEnd(bool quit) {
         m_songInfo->setFromBeatmap(m_selectedBeatmap, m_selectedBeatmap->getSelectedDifficulty2());
 }
 
-void OsuSongBrowser2::onSelectionChange(OsuUISongBrowserButton *button, bool rebuild) {
+void OsuSongBrowser::onSelectionChange(OsuUISongBrowserButton *button, bool rebuild) {
     if(button == NULL) return;
 
     m_contextMenu->setVisible2(false);
@@ -1677,7 +1673,7 @@ void OsuSongBrowser2::onSelectionChange(OsuUISongBrowserButton *button, bool reb
     if(rebuild) rebuildSongButtons();
 }
 
-void OsuSongBrowser2::onDifficultySelected(OsuDatabaseBeatmap *diff2, bool play) {
+void OsuSongBrowser::onDifficultySelected(OsuDatabaseBeatmap *diff2, bool play) {
     // legacy logic (deselect = unload)
     const bool wasSelectedBeatmapNULL = (m_selectedBeatmap == NULL);
     if(m_selectedBeatmap != NULL) m_selectedBeatmap->deselect();
@@ -1745,7 +1741,7 @@ void OsuSongBrowser2::onDifficultySelected(OsuDatabaseBeatmap *diff2, bool play)
     recalculateStarsForSelectedBeatmap();
 }
 
-void OsuSongBrowser2::refreshBeatmaps() {
+void OsuSongBrowser::refreshBeatmaps() {
     if(!m_bVisible || m_bHasSelectedAndIsPlaying) return;
 
     // reset
@@ -1820,7 +1816,7 @@ void OsuSongBrowser2::refreshBeatmaps() {
     m_db->load();
 }
 
-void OsuSongBrowser2::addBeatmap(OsuDatabaseBeatmap *beatmap) {
+void OsuSongBrowser::addBeatmap(OsuDatabaseBeatmap *beatmap) {
     if(beatmap->getDifficulties().size() < 1) return;
 
     OsuUISongBrowserSongButton *songButton;
@@ -1950,7 +1946,7 @@ void OsuSongBrowser2::addBeatmap(OsuDatabaseBeatmap *beatmap) {
     }
 }
 
-void OsuSongBrowser2::readdBeatmap(OsuDatabaseBeatmap *diff2) {
+void OsuSongBrowser::readdBeatmap(OsuDatabaseBeatmap *diff2) {
     // this function readds updated diffs to the correct groups
     // i.e. when stars and length are calculated in background
 
@@ -2029,7 +2025,7 @@ void OsuSongBrowser2::readdBeatmap(OsuDatabaseBeatmap *diff2) {
     }
 }
 
-void OsuSongBrowser2::requestNextScrollToSongButtonJumpFix(OsuUISongBrowserSongDifficultyButton *diffButton) {
+void OsuSongBrowser::requestNextScrollToSongButtonJumpFix(OsuUISongBrowserSongDifficultyButton *diffButton) {
     if(diffButton == NULL) return;
 
     m_bNextScrollToSongButtonJumpFixScheduled = true;
@@ -2039,7 +2035,7 @@ void OsuSongBrowser2::requestNextScrollToSongButtonJumpFix(OsuUISongBrowserSongD
     m_fNextScrollToSongButtonJumpFixOldScrollSizeY = m_songBrowser->getScrollSize().y;
 }
 
-void OsuSongBrowser2::scrollToSongButton(OsuUISongBrowserButton *songButton, bool alignOnTop) {
+void OsuSongBrowser::scrollToSongButton(OsuUISongBrowserButton *songButton, bool alignOnTop) {
     if(songButton == NULL) return;
 
     // NOTE: compensate potential scroll jump due to added/removed elements (feels a lot better this way, also easier on
@@ -2063,7 +2059,7 @@ void OsuSongBrowser2::scrollToSongButton(OsuUISongBrowserButton *songButton, boo
                              (alignOnTop ? (0) : (m_songBrowser->getSize().y / 2 - songButton->getSize().y / 2)));
 }
 
-OsuUISongBrowserButton *OsuSongBrowser2::findCurrentlySelectedSongButton() const {
+OsuUISongBrowserButton *OsuSongBrowser::findCurrentlySelectedSongButton() const {
     OsuUISongBrowserButton *selectedButton = NULL;
     const std::vector<CBaseUIElement *> &elements = m_songBrowser->getContainer()->getElements();
     for(size_t i = 0; i < elements.size(); i++) {
@@ -2074,12 +2070,12 @@ OsuUISongBrowserButton *OsuSongBrowser2::findCurrentlySelectedSongButton() const
     return selectedButton;
 }
 
-void OsuSongBrowser2::scrollToSelectedSongButton() {
+void OsuSongBrowser::scrollToSelectedSongButton() {
     auto selectedButton = findCurrentlySelectedSongButton();
     scrollToSongButton(selectedButton);
 }
 
-void OsuSongBrowser2::rebuildSongButtons() {
+void OsuSongBrowser::rebuildSongButtons() {
     m_songBrowser->getContainer()->empty();
 
     // NOTE: currently supports 3 depth layers (collection > beatmap > diffs)
@@ -2145,7 +2141,7 @@ void OsuSongBrowser2::rebuildSongButtons() {
     updateSongButtonLayout();
 }
 
-void OsuSongBrowser2::updateSongButtonLayout() {
+void OsuSongBrowser::updateSongButtonLayout() {
     // this rebuilds the entire songButton layout (songButtons in relation to others)
     // only the y axis is set, because the x axis is constantly animated and handled within the button classes
     // themselves
@@ -2195,10 +2191,10 @@ void OsuSongBrowser2::updateSongButtonLayout() {
     m_songBrowser->setScrollSizeToContent(m_songBrowser->getSize().y / 2);
 }
 
-void OsuSongBrowser2::updateSongButtonSorting() { onSortChange(osu_songbrowser_scores_sortingtype.getString()); }
+void OsuSongBrowser::updateSongButtonSorting() { onSortChange(osu_songbrowser_scores_sortingtype.getString()); }
 
-bool OsuSongBrowser2::searchMatcher(const OsuDatabaseBeatmap *databaseBeatmap,
-                                    const std::vector<UString> &searchStringTokens) {
+bool OsuSongBrowser::searchMatcher(const OsuDatabaseBeatmap *databaseBeatmap,
+                                   const std::vector<UString> &searchStringTokens) {
     if(databaseBeatmap == NULL) return false;
 
     const std::vector<OsuDatabaseBeatmap *> &diffs = databaseBeatmap->getDifficulties();
@@ -2465,7 +2461,7 @@ bool OsuSongBrowser2::searchMatcher(const OsuDatabaseBeatmap *databaseBeatmap,
     return expressionMatches;
 }
 
-bool OsuSongBrowser2::findSubstringInDifficulty(const OsuDatabaseBeatmap *diff, const UString &searchString) {
+bool OsuSongBrowser::findSubstringInDifficulty(const OsuDatabaseBeatmap *diff, const UString &searchString) {
     if(diff->getTitle().length() > 0) {
         if(diff->getTitle().findIgnoreCase(searchString) != -1) return true;
     }
@@ -2503,7 +2499,7 @@ bool OsuSongBrowser2::findSubstringInDifficulty(const OsuDatabaseBeatmap *diff,
     return false;
 }
 
-void OsuSongBrowser2::updateLayout() {
+void OsuSongBrowser::updateLayout() {
     OsuScreenBackable::updateLayout();
 
     const float uiScale = Osu::ui_scale->getFloat();
@@ -2692,12 +2688,12 @@ void OsuSongBrowser2::updateLayout() {
     m_search->setSize(m_songBrowser->getSize());
 }
 
-void OsuSongBrowser2::onBack() {
+void OsuSongBrowser::onBack() {
     engine->getSound()->play(m_osu->getSkin()->getMenuClick());
     m_osu->toggleSongBrowser();
 }
 
-void OsuSongBrowser2::updateScoreBrowserLayout() {
+void OsuSongBrowser::updateScoreBrowserLayout() {
     const float dpiScale = Osu::getUIScale(m_osu);
 
     const bool shouldScoreBrowserBeVisible =
@@ -2769,7 +2765,7 @@ void OsuSongBrowser2::updateScoreBrowserLayout() {
     m_scoreBrowser->setScrollSizeToContent();
 }
 
-void OsuSongBrowser2::rebuildScoreButtons() {
+void OsuSongBrowser::rebuildScoreButtons() {
     if(!isVisible()) return;
 
     // XXX: When online, it would be nice to scroll to the current user's highscore
@@ -2784,12 +2780,11 @@ void OsuSongBrowser2::rebuildScoreButtons() {
     auto cv_sortingtype = convar->getConVarByName("osu_songbrowser_scores_sortingtype");
     bool is_online = cv_sortingtype->getString() == UString("Online Leaderboard");
 
-    std::vector<OsuDatabase::Score> scores;
+    std::vector<Score> scores;
     if(validBeatmap) {
         auto local_scores = (*m_db->getScores())[m_selectedBeatmap->getSelectedDifficulty2()->getMD5Hash()];
-        auto local_best = std::max_element(
-            local_scores.begin(), local_scores.end(),
-            [](OsuDatabase::Score const &a, OsuDatabase::Score const &b) { return a.score < b.score; });
+        auto local_best = std::max_element(local_scores.begin(), local_scores.end(),
+                                           [](Score const &a, Score const &b) { return a.score < b.score; });
 
         if(is_online) {
             auto search = m_db->m_online_scores.find(m_selectedBeatmap->getSelectedDifficulty2()->getMD5Hash());
@@ -2809,7 +2804,7 @@ void OsuSongBrowser2::rebuildScoreButtons() {
                     SAFE_DELETE(m_localBestButton);
                     m_localBestButton = new OsuUISongBrowserScoreButton(m_osu, m_contextMenu, 0, 0, 0, 0);
                     m_localBestButton->setClickCallback(
-                        fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onScoreClicked));
+                        fastdelegate::MakeDelegate(this, &OsuSongBrowser::onScoreClicked));
                     m_localBestButton->map_hash = m_selectedBeatmap->getSelectedDifficulty2()->getMD5Hash();
                     m_localBestButton->setScore(*local_best, m_selectedBeatmap->getSelectedDifficulty2());
                     m_localBestButton->resetHighlight();
@@ -2831,7 +2826,7 @@ void OsuSongBrowser2::rebuildScoreButtons() {
                     SAFE_DELETE(m_localBestButton);
                     m_localBestButton = new OsuUISongBrowserScoreButton(m_osu, m_contextMenu, 0, 0, 0, 0);
                     m_localBestButton->setClickCallback(
-                        fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onScoreClicked));
+                        fastdelegate::MakeDelegate(this, &OsuSongBrowser::onScoreClicked));
                     m_localBestButton->map_hash = m_selectedBeatmap->getSelectedDifficulty2()->getMD5Hash();
                     m_localBestButton->setScore(*local_best, m_selectedBeatmap->getSelectedDifficulty2());
                     m_localBestButton->resetHighlight();
@@ -2853,7 +2848,7 @@ void OsuSongBrowser2::rebuildScoreButtons() {
         for(size_t i = 0; i < numNewButtons; i++) {
             OsuUISongBrowserScoreButton *scoreButton =
                 new OsuUISongBrowserScoreButton(m_osu, m_contextMenu, 0, 0, 0, 0);
-            scoreButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onScoreClicked));
+            scoreButton->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onScoreClicked));
             m_scoreButtonCache.push_back(scoreButton);
         }
     }
@@ -2898,11 +2893,11 @@ void OsuSongBrowser2::rebuildScoreButtons() {
     updateScoreBrowserLayout();
 }
 
-void OsuSongBrowser2::scheduleSearchUpdate(bool immediately) {
+void OsuSongBrowser::scheduleSearchUpdate(bool immediately) {
     m_fSearchWaitTime = engine->getTime() + (immediately ? 0.0f : osu_songbrowser_search_delay.getFloat());
 }
 
-void OsuSongBrowser2::checkHandleKillBackgroundStarCalculator() {
+void OsuSongBrowser::checkHandleKillBackgroundStarCalculator() {
     if(!m_backgroundStarCalculator->isDead()) {
         m_backgroundStarCalculator->kill();
 
@@ -2916,7 +2911,7 @@ void OsuSongBrowser2::checkHandleKillBackgroundStarCalculator() {
     }
 }
 
-bool OsuSongBrowser2::checkHandleKillDynamicStarCalculator(bool timeout) {
+bool OsuSongBrowser::checkHandleKillDynamicStarCalculator(bool timeout) {
     if(!m_dynamicStarCalculator->isDead()) {
         m_dynamicStarCalculator->kill();
 
@@ -2938,7 +2933,7 @@ bool OsuSongBrowser2::checkHandleKillDynamicStarCalculator(bool timeout) {
                 m_dynamicStarCalculator->isAsyncReady());
 }
 
-void OsuSongBrowser2::checkHandleKillBackgroundSearchMatcher() {
+void OsuSongBrowser::checkHandleKillBackgroundSearchMatcher() {
     if(!m_backgroundSearchMatcher->isDead()) {
         m_backgroundSearchMatcher->kill();
 
@@ -2952,15 +2947,15 @@ void OsuSongBrowser2::checkHandleKillBackgroundSearchMatcher() {
     }
 }
 
-OsuUISelectionButton *OsuSongBrowser2::addBottombarNavButton(std::function<OsuSkinImage *()> getImageFunc,
-                                                             std::function<OsuSkinImage *()> getImageOverFunc) {
+OsuUISelectionButton *OsuSongBrowser::addBottombarNavButton(std::function<OsuSkinImage *()> getImageFunc,
+                                                            std::function<OsuSkinImage *()> getImageOverFunc) {
     OsuUISelectionButton *btn = new OsuUISelectionButton(getImageFunc, getImageOverFunc, 0, 0, 0, 0, "");
     m_bottombar->addBaseUIElement(btn);
     m_bottombarNavButtons.push_back(btn);
     return btn;
 }
 
-CBaseUIButton *OsuSongBrowser2::addTopBarRightTabButton(UString text) {
+CBaseUIButton *OsuSongBrowser::addTopBarRightTabButton(UString text) {
     // sanity check
     {
         bool isValid = false;
@@ -2983,7 +2978,7 @@ CBaseUIButton *OsuSongBrowser2::addTopBarRightTabButton(UString text) {
     return btn;
 }
 
-CBaseUIButton *OsuSongBrowser2::addTopBarRightGroupButton(UString text) {
+CBaseUIButton *OsuSongBrowser::addTopBarRightGroupButton(UString text) {
     CBaseUIButton *btn = new CBaseUIButton(0, 0, 0, 0, "", text);
     btn->setDrawBackground(false);
     m_topbarRight->addBaseUIElement(btn);
@@ -2991,7 +2986,7 @@ CBaseUIButton *OsuSongBrowser2::addTopBarRightGroupButton(UString text) {
     return btn;
 }
 
-CBaseUIButton *OsuSongBrowser2::addTopBarRightSortButton(UString text) {
+CBaseUIButton *OsuSongBrowser::addTopBarRightSortButton(UString text) {
     CBaseUIButton *btn = new CBaseUIButton(0, 0, 0, 0, "", text);
     btn->setDrawBackground(false);
     m_topbarRight->addBaseUIElement(btn);
@@ -2999,7 +2994,7 @@ CBaseUIButton *OsuSongBrowser2::addTopBarRightSortButton(UString text) {
     return btn;
 }
 
-CBaseUIButton *OsuSongBrowser2::addTopBarLeftTabButton(UString text) {
+CBaseUIButton *OsuSongBrowser::addTopBarLeftTabButton(UString text) {
     CBaseUIButton *btn = new CBaseUIButton(0, 0, 0, 0, "", text);
     btn->setDrawBackground(false);
     m_topbarLeft->addBaseUIElement(btn);
@@ -3007,7 +3002,7 @@ CBaseUIButton *OsuSongBrowser2::addTopBarLeftTabButton(UString text) {
     return btn;
 }
 
-CBaseUIButton *OsuSongBrowser2::addTopBarLeftButton(UString text) {
+CBaseUIButton *OsuSongBrowser::addTopBarLeftButton(UString text) {
     CBaseUIButton *btn = new CBaseUIButton(0, 0, 0, 0, "", text);
     btn->setDrawBackground(false);
     m_topbarLeft->addBaseUIElement(btn);
@@ -3015,11 +3010,11 @@ CBaseUIButton *OsuSongBrowser2::addTopBarLeftButton(UString text) {
     return btn;
 }
 
-void OsuSongBrowser2::onDatabaseLoadingFinished() {
+void OsuSongBrowser::onDatabaseLoadingFinished() {
     m_beatmaps = std::vector<OsuDatabaseBeatmap *>(
         m_db->getDatabaseBeatmaps());  // having a copy of the vector in here is actually completely unnecessary
 
-    debugLog("OsuSongBrowser2::onDatabaseLoadingFinished() : %i beatmaps.\n", m_beatmaps.size());
+    debugLog("OsuSongBrowser::onDatabaseLoadingFinished() : %i beatmaps.\n", m_beatmaps.size());
 
     // initialize all collection (grouped) buttons
     {
@@ -3210,7 +3205,7 @@ void OsuSongBrowser2::onDatabaseLoadingFinished() {
     }
 }
 
-void OsuSongBrowser2::onSearchUpdate() {
+void OsuSongBrowser::onSearchUpdate() {
     const bool hasHardcodedSearchStringChanged =
         (m_sPrevHardcodedSearchString != osu_songbrowser_search_hardcoded_filter.getString());
     const bool hasSearchStringChanged = (m_sPrevSearchString != m_sSearchString);
@@ -3271,8 +3266,8 @@ void OsuSongBrowser2::onSearchUpdate() {
     m_sPrevHardcodedSearchString = osu_songbrowser_search_hardcoded_filter.getString();
 }
 
-void OsuSongBrowser2::rebuildSongButtonsAndVisibleSongButtonsWithSearchMatchSupport(bool scrollToTop,
-                                                                                    bool doRebuildSongButtons) {
+void OsuSongBrowser::rebuildSongButtonsAndVisibleSongButtonsWithSearchMatchSupport(bool scrollToTop,
+                                                                                   bool doRebuildSongButtons) {
     // reset container and visible buttons list
     m_songBrowser->getContainer()->empty();
     m_visibleSongButtons.clear();
@@ -3369,7 +3364,7 @@ void OsuSongBrowser2::rebuildSongButtonsAndVisibleSongButtonsWithSearchMatchSupp
     }
 }
 
-void OsuSongBrowser2::onSortScoresClicked(CBaseUIButton *button) {
+void OsuSongBrowser::onSortScoresClicked(CBaseUIButton *button) {
     m_contextMenu->setPos(button->getPos());
     m_contextMenu->setRelPos(button->getRelPos());
     m_contextMenu->begin(button->getSize().x);
@@ -3386,10 +3381,10 @@ void OsuSongBrowser2::onSortScoresClicked(CBaseUIButton *button) {
         }
     }
     m_contextMenu->end(false, false);
-    m_contextMenu->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onSortScoresChange));
+    m_contextMenu->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onSortScoresChange));
 }
 
-void OsuSongBrowser2::onSortScoresChange(UString text, int id) {
+void OsuSongBrowser::onSortScoresChange(UString text, int id) {
     osu_songbrowser_scores_sortingtype.setValue(text);  // NOTE: remember
     m_scoreSortButton->setText(text);
     rebuildScoreButtons();
@@ -3413,14 +3408,14 @@ void OsuSongBrowser2::onSortScoresChange(UString text, int id) {
     }
 }
 
-void OsuSongBrowser2::onWebClicked(CBaseUIButton *button) {
+void OsuSongBrowser::onWebClicked(CBaseUIButton *button) {
     if(m_songInfo->getBeatmapID() > 0) {
         env->openURLInDefaultBrowser(UString::format("https://osu.ppy.sh/b/%ld", m_songInfo->getBeatmapID()));
         m_osu->getNotificationOverlay()->addNotification("Opening browser, please wait ...", 0xffffffff, false, 0.75f);
     }
 }
 
-void OsuSongBrowser2::onGroupClicked(CBaseUIButton *button) {
+void OsuSongBrowser::onGroupClicked(CBaseUIButton *button) {
     m_contextMenu->setPos(button->getPos());
     m_contextMenu->setRelPos(button->getRelPos());
     m_contextMenu->begin(button->getSize().x);
@@ -3431,10 +3426,10 @@ void OsuSongBrowser2::onGroupClicked(CBaseUIButton *button) {
         }
     }
     m_contextMenu->end(false, false);
-    m_contextMenu->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onGroupChange));
+    m_contextMenu->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onGroupChange));
 }
 
-void OsuSongBrowser2::onGroupChange(UString text, int id) {
+void OsuSongBrowser::onGroupChange(UString text, int id) {
     GROUPING *grouping = (m_groupings.size() > 0 ? &m_groupings[0] : NULL);
     for(size_t i = 0; i < m_groupings.size(); i++) {
         if(m_groupings[i].id == id || (text.length() > 1 && m_groupings[i].name == text)) {
@@ -3495,7 +3490,7 @@ void OsuSongBrowser2::onGroupChange(UString text, int id) {
     }
 }
 
-void OsuSongBrowser2::onSortClicked(CBaseUIButton *button) {
+void OsuSongBrowser::onSortClicked(CBaseUIButton *button) {
     m_contextMenu->setPos(button->getPos());
     m_contextMenu->setRelPos(button->getRelPos());
     m_contextMenu->begin(button->getSize().x);
@@ -3506,7 +3501,7 @@ void OsuSongBrowser2::onSortClicked(CBaseUIButton *button) {
         }
     }
     m_contextMenu->end(false, false);
-    m_contextMenu->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onSortChange));
+    m_contextMenu->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onSortChange));
 
     // NOTE: don't remember group setting on shutdown
 
@@ -3518,9 +3513,9 @@ void OsuSongBrowser2::onSortClicked(CBaseUIButton *button) {
     }
 }
 
-void OsuSongBrowser2::onSortChange(UString text, int id) { onSortChangeInt(text, true); }
+void OsuSongBrowser::onSortChange(UString text, int id) { onSortChangeInt(text, true); }
 
-void OsuSongBrowser2::onSortChangeInt(UString text, bool autoScroll) {
+void OsuSongBrowser::onSortChangeInt(UString text, bool autoScroll) {
     SORTING_METHOD *sortingMethod = (m_sortingMethods.size() > 3 ? &m_sortingMethods[3] : NULL);
     for(size_t i = 0; i < m_sortingMethods.size(); i++) {
         if(m_sortingMethods[i].name == text) {
@@ -3617,11 +3612,11 @@ void OsuSongBrowser2::onSortChangeInt(UString text, bool autoScroll) {
     onAfterSortingOrGroupChange(autoScroll);
 }
 
-void OsuSongBrowser2::onGroupTabButtonClicked(CBaseUIButton *groupTabButton) {
+void OsuSongBrowser::onGroupTabButtonClicked(CBaseUIButton *groupTabButton) {
     onGroupChange(groupTabButton->getText());
 }
 
-void OsuSongBrowser2::onGroupNoGrouping() {
+void OsuSongBrowser::onGroupNoGrouping() {
     m_group = GROUP::GROUP_NO_GROUPING;
 
     m_visibleSongButtons.clear();
@@ -3631,7 +3626,7 @@ void OsuSongBrowser2::onGroupNoGrouping() {
     onAfterSortingOrGroupChange();
 }
 
-void OsuSongBrowser2::onGroupCollections(bool autoScroll) {
+void OsuSongBrowser::onGroupCollections(bool autoScroll) {
     m_group = GROUP::GROUP_COLLECTIONS;
 
     m_visibleSongButtons.clear();
@@ -3641,7 +3636,7 @@ void OsuSongBrowser2::onGroupCollections(bool autoScroll) {
     onAfterSortingOrGroupChange(autoScroll);
 }
 
-void OsuSongBrowser2::onGroupArtist() {
+void OsuSongBrowser::onGroupArtist() {
     m_group = GROUP::GROUP_ARTIST;
 
     m_visibleSongButtons.clear();
@@ -3652,7 +3647,7 @@ void OsuSongBrowser2::onGroupArtist() {
     onAfterSortingOrGroupChange();
 }
 
-void OsuSongBrowser2::onGroupDifficulty() {
+void OsuSongBrowser::onGroupDifficulty() {
     m_group = GROUP::GROUP_DIFFICULTY;
 
     m_visibleSongButtons.clear();
@@ -3663,7 +3658,7 @@ void OsuSongBrowser2::onGroupDifficulty() {
     onAfterSortingOrGroupChange();
 }
 
-void OsuSongBrowser2::onGroupBPM() {
+void OsuSongBrowser::onGroupBPM() {
     m_group = GROUP::GROUP_BPM;
 
     m_visibleSongButtons.clear();
@@ -3674,7 +3669,7 @@ void OsuSongBrowser2::onGroupBPM() {
     onAfterSortingOrGroupChange();
 }
 
-void OsuSongBrowser2::onGroupCreator() {
+void OsuSongBrowser::onGroupCreator() {
     m_group = GROUP::GROUP_CREATOR;
 
     m_visibleSongButtons.clear();
@@ -3685,7 +3680,7 @@ void OsuSongBrowser2::onGroupCreator() {
     onAfterSortingOrGroupChange();
 }
 
-void OsuSongBrowser2::onGroupDateadded() {
+void OsuSongBrowser::onGroupDateadded() {
     m_group = GROUP::GROUP_DATEADDED;
 
     m_visibleSongButtons.clear();
@@ -3696,7 +3691,7 @@ void OsuSongBrowser2::onGroupDateadded() {
     onAfterSortingOrGroupChange();
 }
 
-void OsuSongBrowser2::onGroupLength() {
+void OsuSongBrowser::onGroupLength() {
     m_group = GROUP::GROUP_LENGTH;
 
     m_visibleSongButtons.clear();
@@ -3707,7 +3702,7 @@ void OsuSongBrowser2::onGroupLength() {
     onAfterSortingOrGroupChange();
 }
 
-void OsuSongBrowser2::onGroupTitle() {
+void OsuSongBrowser::onGroupTitle() {
     m_group = GROUP::GROUP_TITLE;
 
     m_visibleSongButtons.clear();
@@ -3718,7 +3713,7 @@ void OsuSongBrowser2::onGroupTitle() {
     onAfterSortingOrGroupChange();
 }
 
-void OsuSongBrowser2::onAfterSortingOrGroupChange(bool autoScroll) {
+void OsuSongBrowser::onAfterSortingOrGroupChange(bool autoScroll) {
     // keep search state consistent between tab changes
     if(m_bInSearch) onSearchUpdate();
 
@@ -3727,7 +3722,7 @@ void OsuSongBrowser2::onAfterSortingOrGroupChange(bool autoScroll) {
     m_bOnAfterSortingOrGroupChangeUpdateScheduled = true;
 }
 
-void OsuSongBrowser2::onAfterSortingOrGroupChangeUpdateInt(bool autoScroll) {
+void OsuSongBrowser::onAfterSortingOrGroupChangeUpdateInt(bool autoScroll) {
     // if anything was selected, scroll to that. otherwise scroll to top
     const std::vector<CBaseUIElement *> &elements = m_songBrowser->getContainer()->getElements();
     bool isAnythingSelected = false;
@@ -3747,7 +3742,7 @@ void OsuSongBrowser2::onAfterSortingOrGroupChangeUpdateInt(bool autoScroll) {
     }
 }
 
-void OsuSongBrowser2::onSelectionMode() {
+void OsuSongBrowser::onSelectionMode() {
     engine->getSound()->play(m_osu->getSkin()->getMenuClick());
 
     m_contextMenu->setPos(m_bottombarNavButtons[0]->getPos());
@@ -3779,15 +3774,15 @@ void OsuSongBrowser2::onSelectionMode() {
                              Vector2((m_contextMenu->getSize().x - m_bottombarNavButtons[0]->getSize().x) / 2.0f,
                                      m_contextMenu->getSize().y));
     m_contextMenu->end(true, false);
-    m_contextMenu->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onModeChange2));
+    m_contextMenu->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onModeChange2));
 }
 
-void OsuSongBrowser2::onSelectionMods() {
+void OsuSongBrowser::onSelectionMods() {
     engine->getSound()->play(m_osu->getSkin()->getMenuClick());
     m_osu->toggleModSelection(m_bF1Pressed);
 }
 
-void OsuSongBrowser2::onSelectionRandom() {
+void OsuSongBrowser::onSelectionRandom() {
     engine->getSound()->play(m_osu->getSkin()->getMenuClick());
     if(m_bShiftPressed)
         m_bPreviousRandomBeatmapScheduled = true;
@@ -3795,7 +3790,7 @@ void OsuSongBrowser2::onSelectionRandom() {
         m_bRandomBeatmapScheduled = true;
 }
 
-void OsuSongBrowser2::onSelectionOptions() {
+void OsuSongBrowser::onSelectionOptions() {
     engine->getSound()->play(m_osu->getSkin()->getMenuClick());
 
     OsuUISongBrowserButton *currentlySelectedSongButton = findCurrentlySelectedSongButton();
@@ -3816,13 +3811,13 @@ void OsuSongBrowser2::onSelectionOptions() {
     }
 }
 
-void OsuSongBrowser2::onModeChange(UString text) { onModeChange2(text); }
+void OsuSongBrowser::onModeChange(UString text) { onModeChange2(text); }
 
-void OsuSongBrowser2::onModeChange2(UString text, int id) {
+void OsuSongBrowser::onModeChange2(UString text, int id) {
     m_osu_mod_fposu_ref->setValue(id == 2 || text == UString("fposu"));
 }
 
-void OsuSongBrowser2::onUserButtonClicked() {
+void OsuSongBrowser::onUserButtonClicked() {
     // Not allowed to switch user while online
     if(bancho.user_id > 0) return;
 
@@ -3849,12 +3844,12 @@ void OsuSongBrowser2::onUserButtonClicked() {
         m_contextMenu->setPos(m_contextMenu->getPos() - Vector2(0, m_contextMenu->getSize().y));
         m_contextMenu->setRelPos(m_contextMenu->getRelPos() - Vector2(0, m_contextMenu->getSize().y));
         m_contextMenu->end(true, true);
-        m_contextMenu->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser2::onUserButtonChange));
+        m_contextMenu->setClickCallback(fastdelegate::MakeDelegate(this, &OsuSongBrowser::onUserButtonChange));
         OsuUIContextMenu::clampToRightScreenEdge(m_contextMenu);
     }
 }
 
-void OsuSongBrowser2::onUserButtonChange(UString text, int id) {
+void OsuSongBrowser::onUserButtonChange(UString text, int id) {
     if(id == 0) return;
 
     if(id == 1) {
@@ -3869,7 +3864,7 @@ void OsuSongBrowser2::onUserButtonChange(UString text, int id) {
     m_userButton->updateUserStats();
 }
 
-void OsuSongBrowser2::onScoreClicked(CBaseUIButton *button) {
+void OsuSongBrowser::onScoreClicked(CBaseUIButton *button) {
     OsuUISongBrowserScoreButton *scoreButton = (OsuUISongBrowserScoreButton *)button;
 
     // NOTE: the order of these two calls matters (score data overwrites relevant fields, but base values are coming
@@ -3881,7 +3876,7 @@ void OsuSongBrowser2::onScoreClicked(CBaseUIButton *button) {
     m_osu->getRankingScreen()->setVisible(true);
 }
 
-void OsuSongBrowser2::onScoreContextMenu(OsuUISongBrowserScoreButton *scoreButton, int id) {
+void OsuSongBrowser::onScoreContextMenu(OsuUISongBrowserScoreButton *scoreButton, int id) {
     // NOTE: see OsuUISongBrowserScoreButton::onContextMenu()
 
     if(id == 2) {
@@ -3892,8 +3887,8 @@ void OsuSongBrowser2::onScoreContextMenu(OsuUISongBrowserScoreButton *scoreButto
     }
 }
 
-void OsuSongBrowser2::onSongButtonContextMenu(OsuUISongBrowserSongButton *songButton, UString text, int id) {
-    // debugLog("OsuSongBrowser2::onSongButtonContextMenu(%p, %s, %i)\n", songButton, text.toUtf8(), id);
+void OsuSongBrowser::onSongButtonContextMenu(OsuUISongBrowserSongButton *songButton, UString text, int id) {
+    // debugLog("OsuSongBrowser::onSongButtonContextMenu(%p, %s, %i)\n", songButton, text.toUtf8(), id);
 
     struct CollectionManagementHelper {
         static std::vector<MD5Hash> getBeatmapSetHashesForSongButton(OsuUISongBrowserSongButton *songButton,
@@ -4033,8 +4028,8 @@ void OsuSongBrowser2::onSongButtonContextMenu(OsuUISongBrowserSongButton *songBu
     }
 }
 
-void OsuSongBrowser2::onCollectionButtonContextMenu(OsuUISongBrowserCollectionButton *collectionButton, UString text,
-                                                    int id) {
+void OsuSongBrowser::onCollectionButtonContextMenu(OsuUISongBrowserCollectionButton *collectionButton, UString text,
+                                                   int id) {
     if(id == 2)  // delete collection
     {
         for(size_t i = 0; i < m_collectionButtons.size(); i++) {
@@ -4061,7 +4056,7 @@ void OsuSongBrowser2::onCollectionButtonContextMenu(OsuUISongBrowserCollectionBu
     }
 }
 
-void OsuSongBrowser2::highlightScore(uint64_t unixTimestamp) {
+void OsuSongBrowser::highlightScore(uint64_t unixTimestamp) {
     for(size_t i = 0; i < m_scoreButtonCache.size(); i++) {
         if(m_scoreButtonCache[i]->getScore().unixTimestamp == unixTimestamp) {
             m_scoreBrowser->scrollToElement(m_scoreButtonCache[i], 0, 10);
@@ -4071,11 +4066,11 @@ void OsuSongBrowser2::highlightScore(uint64_t unixTimestamp) {
     }
 }
 
-void OsuSongBrowser2::recalculateStarsForSelectedBeatmap(bool force) {
+void OsuSongBrowser::recalculateStarsForSelectedBeatmap(bool force) {
     if(!osu_songbrowser_dynamic_star_recalc.getBool()) return;
     if(m_selectedBeatmap == NULL || m_selectedBeatmap->getSelectedDifficulty2() == NULL) return;
 
-    // HACKHACK: temporarily deactivated, see OsuSongBrowser2::update(), but only if drawing scrubbing timeline strain
+    // HACKHACK: temporarily deactivated, see OsuSongBrowser::update(), but only if drawing scrubbing timeline strain
     // graph is enabled (or "Draw Stats: Stars* (Total)", or "Draw Stats: pp (SS)")
     if(!m_osu_draw_scrubbing_timeline_strain_graph_ref->getBool() && !m_osu_draw_statistics_perfectpp_ref->getBool() &&
        !m_osu_draw_statistics_totalstars_ref->getBool()) {
@@ -4114,14 +4109,14 @@ void OsuSongBrowser2::recalculateStarsForSelectedBeatmap(bool force) {
     }
 }
 
-void OsuSongBrowser2::selectSongButton(OsuUISongBrowserButton *songButton) {
+void OsuSongBrowser::selectSongButton(OsuUISongBrowserButton *songButton) {
     if(songButton != NULL && !songButton->isSelected()) {
         m_contextMenu->setVisible2(false);
         songButton->select();
     }
 }
 
-void OsuSongBrowser2::selectRandomBeatmap(bool playMusicFromPreviewPoint) {
+void OsuSongBrowser::selectRandomBeatmap(bool playMusicFromPreviewPoint) {
     // filter songbuttons or independent diffs
     const std::vector<CBaseUIElement *> &elements = m_songBrowser->getContainer()->getElements();
     std::vector<OsuUISongBrowserSongButton *> songButtons;
@@ -4149,7 +4144,7 @@ void OsuSongBrowser2::selectRandomBeatmap(bool playMusicFromPreviewPoint) {
     selectSongButton(songButton);
 }
 
-void OsuSongBrowser2::selectPreviousRandomBeatmap() {
+void OsuSongBrowser::selectPreviousRandomBeatmap() {
     if(m_previousRandomBeatmaps.size() > 0) {
         OsuDatabaseBeatmap *currentRandomBeatmap = m_previousRandomBeatmaps.back();
         if(m_previousRandomBeatmaps.size() > 1 && m_selectedBeatmap != NULL &&
@@ -4197,7 +4192,7 @@ void OsuSongBrowser2::selectPreviousRandomBeatmap() {
     }
 }
 
-void OsuSongBrowser2::playSelectedDifficulty() {
+void OsuSongBrowser::playSelectedDifficulty() {
     const std::vector<CBaseUIElement *> &elements = m_songBrowser->getContainer()->getElements();
     for(size_t i = 0; i < elements.size(); i++) {
         OsuUISongBrowserSongDifficultyButton *songDifficultyButton =
@@ -4209,7 +4204,7 @@ void OsuSongBrowser2::playSelectedDifficulty() {
     }
 }
 
-void OsuSongBrowser2::recreateCollectionsButtons() {
+void OsuSongBrowser::recreateCollectionsButtons() {
     // reset
     {
         m_selectionPreviousCollectionButton = NULL;

+ 3 - 3
src/App/Osu/OsuSongBrowser2.h → src/App/Osu/OsuSongBrowser.h

@@ -41,7 +41,7 @@ class ConVar;
 
 class OsuSongBrowserBackgroundSearchMatcher;
 
-class OsuSongBrowser2 : public OsuScreenBackable {
+class OsuSongBrowser : public OsuScreenBackable {
    public:
     static void drawSelectedBeatmapBackgroundImage(Graphics *g, Osu *osu, float alpha = 1.0f);
 
@@ -99,8 +99,8 @@ class OsuSongBrowser2 : public OsuScreenBackable {
 
     friend class OsuSongBrowserBackgroundSearchMatcher;
 
-    OsuSongBrowser2(Osu *osu);
-    virtual ~OsuSongBrowser2();
+    OsuSongBrowser(Osu *osu);
+    virtual ~OsuSongBrowser();
 
     virtual void draw(Graphics *g);
     virtual void mouse_update(bool *propagate_clicks);

+ 1 - 1
src/App/Osu/OsuUIRankingScreenRankingPanel.cpp

@@ -191,7 +191,7 @@ void OsuUIRankingScreenRankingPanel::setScore(OsuScore *score) {
     m_bPerfect = (score->getComboFull() > 0 && m_iCombo >= score->getComboFull());
 }
 
-void OsuUIRankingScreenRankingPanel::setScore(OsuDatabase::Score score) {
+void OsuUIRankingScreenRankingPanel::setScore(Score score) {
     m_iScore = score.score;
     m_iNum300s = score.num300s;
     m_iNum300gs = score.numGekis;

+ 1 - 1
src/App/Osu/OsuUIRankingScreenRankingPanel.h

@@ -22,7 +22,7 @@ class OsuUIRankingScreenRankingPanel : public CBaseUIImage {
     virtual void draw(Graphics *g);
 
     void setScore(OsuScore *score);
-    void setScore(OsuDatabase::Score score);
+    void setScore(Score score);
 
    private:
     void drawHitImage(Graphics *g, OsuSkinImage *img, float scale, Vector2 pos);

+ 2 - 2
src/App/Osu/OsuUISongBrowserButton.cpp

@@ -15,7 +15,7 @@
 #include "Mouse.h"
 #include "Osu.h"
 #include "OsuSkin.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "ResourceManager.h"
 #include "SoundEngine.h"
 
@@ -36,7 +36,7 @@ int OsuUISongBrowserButton::sortHackCounter = 0;
 
 // Color OsuUISongBrowserButton::inactiveDifficultyBackgroundColor = COLOR(255, 0, 150, 236); // blue
 
-OsuUISongBrowserButton::OsuUISongBrowserButton(Osu *osu, OsuSongBrowser2 *songBrowser, CBaseUIScrollView *view,
+OsuUISongBrowserButton::OsuUISongBrowserButton(Osu *osu, OsuSongBrowser *songBrowser, CBaseUIScrollView *view,
                                                OsuUIContextMenu *contextMenu, float xPos, float yPos, float xSize,
                                                float ySize, UString name)
     : CBaseUIButton(xPos, yPos, xSize, ySize, name, "") {

+ 3 - 3
src/App/Osu/OsuUISongBrowserButton.h

@@ -12,14 +12,14 @@
 
 class Osu;
 class OsuDatabaseBeatmap;
-class OsuSongBrowser2;
+class OsuSongBrowser;
 class OsuUIContextMenu;
 
 class CBaseUIScrollView;
 
 class OsuUISongBrowserButton : public CBaseUIButton {
    public:
-    OsuUISongBrowserButton(Osu *osu, OsuSongBrowser2 *songBrowser, CBaseUIScrollView *view,
+    OsuUISongBrowserButton(Osu *osu, OsuSongBrowser *songBrowser, CBaseUIScrollView *view,
                            OsuUIContextMenu *contextMenu, float xPos, float yPos, float xSize, float ySize,
                            UString name);
     virtual ~OsuUISongBrowserButton();
@@ -65,7 +65,7 @@ class OsuUISongBrowserButton : public CBaseUIButton {
 
     Osu *m_osu;
     CBaseUIScrollView *m_view;
-    OsuSongBrowser2 *m_songBrowser;
+    OsuSongBrowser *m_songBrowser;
     OsuUIContextMenu *m_contextMenu;
 
     McFont *m_font;

+ 3 - 3
src/App/Osu/OsuUISongBrowserCollectionButton.cpp

@@ -15,7 +15,7 @@
 #include "OsuDatabase.h"
 #include "OsuNotificationOverlay.h"
 #include "OsuSkin.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuUIContextMenu.h"
 #include "ResourceManager.h"
 
@@ -37,7 +37,7 @@ ConVar osu_songbrowser_button_collection_inactive_color_g("osu_songbrowser_butto
 ConVar osu_songbrowser_button_collection_inactive_color_b("osu_songbrowser_button_collection_inactive_color_b", 143,
                                                           FCVAR_NONE);
 
-OsuUISongBrowserCollectionButton::OsuUISongBrowserCollectionButton(Osu *osu, OsuSongBrowser2 *songBrowser,
+OsuUISongBrowserCollectionButton::OsuUISongBrowserCollectionButton(Osu *osu, OsuSongBrowser *songBrowser,
                                                                    CBaseUIScrollView *view,
                                                                    OsuUIContextMenu *contextMenu, float xPos,
                                                                    float yPos, float xSize, float ySize, UString name,
@@ -88,7 +88,7 @@ void OsuUISongBrowserCollectionButton::onSelected(bool wasSelected, bool autoSel
 void OsuUISongBrowserCollectionButton::onRightMouseUpInside() { triggerContextMenu(engine->getMouse()->getPos()); }
 
 void OsuUISongBrowserCollectionButton::triggerContextMenu(Vector2 pos) {
-    if(m_osu->getSongBrowser()->getGroupingMode() != OsuSongBrowser2::GROUP::GROUP_COLLECTIONS) return;
+    if(m_osu->getSongBrowser()->getGroupingMode() != OsuSongBrowser::GROUP::GROUP_COLLECTIONS) return;
 
     bool isLegacyCollection = false;
     {

+ 1 - 1
src/App/Osu/OsuUISongBrowserCollectionButton.h

@@ -12,7 +12,7 @@
 
 class OsuUISongBrowserCollectionButton : public OsuUISongBrowserButton {
    public:
-    OsuUISongBrowserCollectionButton(Osu *osu, OsuSongBrowser2 *songBrowser, CBaseUIScrollView *view,
+    OsuUISongBrowserCollectionButton(Osu *osu, OsuSongBrowser *songBrowser, CBaseUIScrollView *view,
                                      OsuUIContextMenu *contextMenu, float xPos, float yPos, float xSize, float ySize,
                                      UString name, UString collectionName,
                                      std::vector<OsuUISongBrowserButton *> children);

+ 1 - 1
src/App/Osu/OsuUISongBrowserInfoLabel.cpp

@@ -17,7 +17,7 @@
 #include "OsuGameRules.h"
 #include "OsuNotificationOverlay.h"
 #include "OsuOptionsMenu.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuTooltipOverlay.h"
 #include "ResourceManager.h"
 

+ 13 - 27
src/App/Osu/OsuUISongBrowserScoreButton.cpp

@@ -27,7 +27,7 @@
 #include "OsuReplay.h"
 #include "OsuSkin.h"
 #include "OsuSkinImage.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuTooltipOverlay.h"
 #include "OsuUIAvatar.h"
 #include "OsuUIContextMenu.h"
@@ -71,7 +71,7 @@ OsuUISongBrowserScoreButton::OsuUISongBrowserScoreButton(Osu *osu, OsuUIContextM
     m_iScoreIndexNumber = 1;
     m_iScoreUnixTimestamp = 0;
 
-    m_scoreGrade = OsuScore::GRADE::GRADE_D;
+    m_scoreGrade = Score::Grade::D;
 }
 
 OsuUISongBrowserScoreButton::~OsuUISongBrowserScoreButton() { anim->deleteExistingAnimation(&m_fIndexNumberAnim); }
@@ -564,21 +564,7 @@ void OsuUISongBrowserScoreButton::onContextMenu(UString text, int id) {
     }
 
     if(id == 2) {
-        ReplayExtraInfo *info = (ReplayExtraInfo *)calloc(1, sizeof(ReplayExtraInfo));
-        info->diff2_md5 = m_score.md5hash.hash;
-        info->mod_flags = m_score.modsLegacy;
-        info->username = m_score.playerName;
-        info->player_id = m_score.player_id;
-
-        APIRequest request;
-        request.type = GET_REPLAY;
-        request.path = UString::format("/web/osu-getreplay.php?u=%s&h=%s&m=0&c=%d", bancho.username.toUtf8(),
-                                       bancho.pw_md5.toUtf8(), m_score.online_score_id);
-        request.mime = NULL;
-        request.extra = (uint8_t *)info;
-        send_api_request(request);
-
-        m_osu->m_notificationOverlay->addNotification("Downloading replay...");
+        OsuReplay::load_and_watch(m_score);
         return;
     }
 
@@ -710,7 +696,7 @@ void OsuUISongBrowserScoreButton::onDeleteScoreConfirmed(UString text, int id) {
     if(m_style == STYLE::SCORE_BROWSER)
         m_osu->getSongBrowser()->onScoreContextMenu(this, 2);
     else if(m_style == STYLE::TOP_RANKS) {
-        // in this case, deletion of the actual scores is handled in OsuSongBrowser2::onScoreContextMenu()
+        // in this case, deletion of the actual scores is handled in OsuSongBrowser::onScoreContextMenu()
         // this is nice because it updates the songbrowser scorebrowser too (so if the user closes the top ranks screen
         // everything is in sync, even for the currently selected beatmap)
         m_osu->getSongBrowser()->onScoreContextMenu(this, 2);
@@ -718,7 +704,7 @@ void OsuUISongBrowserScoreButton::onDeleteScoreConfirmed(UString text, int id) {
     }
 }
 
-void OsuUISongBrowserScoreButton::setScore(const OsuDatabase::Score &score, const OsuDatabaseBeatmap *diff2, int index,
+void OsuUISongBrowserScoreButton::setScore(const Score &score, const OsuDatabaseBeatmap *diff2, int index,
                                            UString titleString, float weight) {
     m_score = score;
     m_iScoreIndexNumber = index;
@@ -882,21 +868,21 @@ bool OsuUISongBrowserScoreButton::isContextMenuVisible() {
     return (m_contextMenu != NULL && m_contextMenu->isVisible());
 }
 
-OsuSkinImage *OsuUISongBrowserScoreButton::getGradeImage(Osu *osu, OsuScore::GRADE grade) {
+OsuSkinImage *OsuUISongBrowserScoreButton::getGradeImage(Osu *osu, Score::Grade grade) {
     switch(grade) {
-        case OsuScore::GRADE::GRADE_XH:
+        case Score::Grade::XH:
             return osu->getSkin()->getRankingXHsmall();
-        case OsuScore::GRADE::GRADE_SH:
+        case Score::Grade::SH:
             return osu->getSkin()->getRankingSHsmall();
-        case OsuScore::GRADE::GRADE_X:
+        case Score::Grade::X:
             return osu->getSkin()->getRankingXsmall();
-        case OsuScore::GRADE::GRADE_S:
+        case Score::Grade::S:
             return osu->getSkin()->getRankingSsmall();
-        case OsuScore::GRADE::GRADE_A:
+        case Score::Grade::A:
             return osu->getSkin()->getRankingAsmall();
-        case OsuScore::GRADE::GRADE_B:
+        case Score::Grade::B:
             return osu->getSkin()->getRankingBsmall();
-        case OsuScore::GRADE::GRADE_C:
+        case Score::Grade::C:
             return osu->getSkin()->getRankingCsmall();
         default:
             return osu->getSkin()->getRankingDsmall();

+ 6 - 6
src/App/Osu/OsuUISongBrowserScoreButton.h

@@ -20,7 +20,7 @@ class OsuUIContextMenu;
 
 class OsuUISongBrowserScoreButton : public CBaseUIButton {
    public:
-    static OsuSkinImage *getGradeImage(Osu *osu, OsuScore::GRADE grade);
+    static OsuSkinImage *getGradeImage(Osu *osu, Score::Grade grade);
     static UString getModsStringForDisplay(int mods);
     static UString getModsStringForConVar(int mods);
 
@@ -36,11 +36,11 @@ class OsuUISongBrowserScoreButton : public CBaseUIButton {
     void highlight();
     void resetHighlight();
 
-    void setScore(const OsuDatabase::Score &score, const OsuDatabaseBeatmap *diff2 = NULL, int index = 1,
-                  UString titleString = "", float weight = 1.0f);
+    void setScore(const Score &score, const OsuDatabaseBeatmap *diff2 = NULL, int index = 1, UString titleString = "",
+                  float weight = 1.0f);
     void setIndex(int index) { m_iScoreIndexNumber = index; }
 
-    inline OsuDatabase::Score getScore() const { return m_score; }
+    inline Score getScore() const { return m_score; }
     inline uint64_t getScoreUnixTimestamp() const { return m_score.unixTimestamp; }
     inline unsigned long long getScoreScore() const { return m_score.score; }
     inline float getScorePP() const { return m_score.pp; }
@@ -88,12 +88,12 @@ class OsuUISongBrowserScoreButton : public CBaseUIButton {
     bool m_bRightClickCheck;
 
     // score data
-    OsuDatabase::Score m_score;
+    Score m_score;
 
     int m_iScoreIndexNumber;
     uint64_t m_iScoreUnixTimestamp;
 
-    OsuScore::GRADE m_scoreGrade;
+    Score::Grade m_scoreGrade;
 
     // STYLE::SCORE_BROWSER
     UString m_sScoreTime;

+ 5 - 5
src/App/Osu/OsuUISongBrowserSongButton.cpp

@@ -16,7 +16,7 @@
 #include "OsuNotificationOverlay.h"
 #include "OsuSkin.h"
 #include "OsuSkinImage.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuUIContextMenu.h"
 #include "OsuUISongBrowserCollectionButton.h"
 #include "OsuUISongBrowserScoreButton.h"
@@ -29,7 +29,7 @@ ConVar osu_songbrowser_thumbnail_fade_in_duration("osu_songbrowser_thumbnail_fad
 
 float OsuUISongBrowserSongButton::thumbnailYRatio = 1.333333f;
 
-OsuUISongBrowserSongButton::OsuUISongBrowserSongButton(Osu *osu, OsuSongBrowser2 *songBrowser, CBaseUIScrollView *view,
+OsuUISongBrowserSongButton::OsuUISongBrowserSongButton(Osu *osu, OsuSongBrowser *songBrowser, CBaseUIScrollView *view,
                                                        OsuUIContextMenu *contextMenu, float xPos, float yPos,
                                                        float xSize, float ySize, UString name,
                                                        OsuDatabaseBeatmap *databaseBeatmap)
@@ -37,7 +37,7 @@ OsuUISongBrowserSongButton::OsuUISongBrowserSongButton(Osu *osu, OsuSongBrowser2
     m_databaseBeatmap = databaseBeatmap;
     m_representativeDatabaseBeatmap = NULL;
 
-    m_grade = OsuScore::GRADE::GRADE_D;
+    m_grade = Score::Grade::D;
     m_bHasGrade = false;
 
     // settings
@@ -227,7 +227,7 @@ void OsuUISongBrowserSongButton::drawSubTitle(Graphics *g, float deselectedAlpha
 }
 
 void OsuUISongBrowserSongButton::sortChildren() {
-    std::sort(m_children.begin(), m_children.end(), OsuSongBrowser2::SortByDifficulty());
+    std::sort(m_children.begin(), m_children.end(), OsuSongBrowser::SortByDifficulty());
 }
 
 void OsuUISongBrowserSongButton::updateLayoutEx() {
@@ -291,7 +291,7 @@ void OsuUISongBrowserSongButton::triggerContextMenu(Vector2 pos) {
 
             m_contextMenu->addButton("[+Set]   Add to Collection", 2);
 
-            if(m_osu->getSongBrowser()->getGroupingMode() == OsuSongBrowser2::GROUP::GROUP_COLLECTIONS) {
+            if(m_osu->getSongBrowser()->getGroupingMode() == OsuSongBrowser::GROUP::GROUP_COLLECTIONS) {
                 // get the collection name for this diff/set
                 UString collectionName;
                 {

+ 3 - 3
src/App/Osu/OsuUISongBrowserSongButton.h

@@ -11,12 +11,12 @@
 #include "OsuScore.h"
 #include "OsuUISongBrowserButton.h"
 
-class OsuSongBrowser2;
+class OsuSongBrowser;
 class OsuDatabaseBeatmap;
 
 class OsuUISongBrowserSongButton : public OsuUISongBrowserButton {
    public:
-    OsuUISongBrowserSongButton(Osu *osu, OsuSongBrowser2 *songBrowser, CBaseUIScrollView *view,
+    OsuUISongBrowserSongButton(Osu *osu, OsuSongBrowser *songBrowser, CBaseUIScrollView *view,
                                OsuUIContextMenu *contextMenu, float xPos, float yPos, float xSize, float ySize,
                                UString name, OsuDatabaseBeatmap *databaseBeatmap);
     virtual ~OsuUISongBrowserSongButton();
@@ -64,7 +64,7 @@ class OsuUISongBrowserSongButton : public OsuUISongBrowserButton {
     UString m_sTitle;
     UString m_sArtist;
     UString m_sMapper;
-    OsuScore::GRADE m_grade;
+    Score::Grade m_grade;
     bool m_bHasGrade;
 
     float m_fTextOffset;

+ 4 - 5
src/App/Osu/OsuUISongBrowserSongDifficultyButton.cpp

@@ -16,7 +16,7 @@
 #include "OsuDatabaseBeatmap.h"
 #include "OsuReplay.h"
 #include "OsuSkin.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuUISongBrowserScoreButton.h"
 #include "ResourceManager.h"
 
@@ -32,7 +32,7 @@ ConVar osu_songbrowser_button_difficulty_inactive_color_b("osu_songbrowser_butto
 ConVar *OsuUISongBrowserSongDifficultyButton::m_osu_scores_enabled = NULL;
 ConVar *OsuUISongBrowserSongDifficultyButton::m_osu_songbrowser_dynamic_star_recalc_ref = NULL;
 
-OsuUISongBrowserSongDifficultyButton::OsuUISongBrowserSongDifficultyButton(Osu *osu, OsuSongBrowser2 *songBrowser,
+OsuUISongBrowserSongDifficultyButton::OsuUISongBrowserSongDifficultyButton(Osu *osu, OsuSongBrowser *songBrowser,
                                                                            CBaseUIScrollView *view,
                                                                            OsuUIContextMenu *contextMenu, float xPos,
                                                                            float yPos, float xSize, float ySize,
@@ -212,12 +212,11 @@ void OsuUISongBrowserSongDifficultyButton::updateGrade() {
     }
 
     bool hasGrade = false;
-    OsuScore::GRADE grade;
+    Score::Grade grade;
 
     m_osu->getSongBrowser()->getDatabase()->sortScores(m_databaseBeatmap->getMD5Hash());
     if((*m_osu->getSongBrowser()->getDatabase()->getScores())[m_databaseBeatmap->getMD5Hash()].size() > 0) {
-        const OsuDatabase::Score &score =
-            (*m_osu->getSongBrowser()->getDatabase()->getScores())[m_databaseBeatmap->getMD5Hash()][0];
+        const Score &score = (*m_osu->getSongBrowser()->getDatabase()->getScores())[m_databaseBeatmap->getMD5Hash()][0];
         hasGrade = true;
         grade = OsuScore::calculateGrade(score.num300s, score.num100s, score.num50s, score.numMisses,
                                          score.modsLegacy & ModFlags::Hidden, score.modsLegacy & ModFlags::Flashlight);

+ 1 - 1
src/App/Osu/OsuUISongBrowserSongDifficultyButton.h

@@ -14,7 +14,7 @@ class ConVar;
 
 class OsuUISongBrowserSongDifficultyButton : public OsuUISongBrowserSongButton {
    public:
-    OsuUISongBrowserSongDifficultyButton(Osu *osu, OsuSongBrowser2 *songBrowser, CBaseUIScrollView *view,
+    OsuUISongBrowserSongDifficultyButton(Osu *osu, OsuSongBrowser *songBrowser, CBaseUIScrollView *view,
                                          OsuUIContextMenu *contextMenu, float xPos, float yPos, float xSize,
                                          float ySize, UString name, OsuDatabaseBeatmap *diff2,
                                          OsuUISongBrowserSongButton *parentSongButton);

+ 1 - 1
src/App/Osu/OsuUISongBrowserUserButton.cpp

@@ -17,7 +17,7 @@
 #include "OsuDatabase.h"
 #include "OsuIcons.h"
 #include "OsuSkin.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuTooltipOverlay.h"
 #include "OsuUIAvatar.h"
 #include "ResourceManager.h"

+ 12 - 15
src/App/Osu/OsuUserStatsScreen.cpp

@@ -23,7 +23,7 @@
 #include "OsuOptionsMenu.h"
 #include "OsuReplay.h"
 #include "OsuSkin.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuTooltipOverlay.h"
 #include "OsuUIContextMenu.h"
 #include "OsuUISongBrowserScoreButton.h"
@@ -80,14 +80,13 @@ class OsuUserStatsScreenBackgroundPPRecalculator : public Resource {
     virtual void init() { m_bReady = true; }
 
     virtual void initAsync() {
-        std::unordered_map<MD5Hash, std::vector<OsuDatabase::Score>> *scores =
-            m_osu->getSongBrowser()->getDatabase()->getScores();
+        std::unordered_map<MD5Hash, std::vector<Score>> *scores = m_osu->getSongBrowser()->getDatabase()->getScores();
 
         // count number of scores to recalculate for UI
         size_t numScoresToRecalculate = 0;
         for(const auto &kv : *scores) {
             for(size_t i = 0; i < kv.second.size(); i++) {
-                const OsuDatabase::Score &score = kv.second[i];
+                const Score &score = kv.second[i];
 
                 if((!score.isLegacyScore || m_bImportLegacyScores) && score.playerName == m_sUserName)
                     numScoresToRecalculate++;
@@ -100,7 +99,7 @@ class OsuUserStatsScreenBackgroundPPRecalculator : public Resource {
         // actually recalculate them
         for(auto &kv : *scores) {
             for(size_t i = 0; i < kv.second.size(); i++) {
-                OsuDatabase::Score &score = kv.second[i];
+                Score &score = kv.second[i];
 
                 if((!score.isLegacyScore || m_bImportLegacyScores) && score.playerName == m_sUserName) {
                     if(m_bInterrupted.load()) break;
@@ -108,7 +107,7 @@ class OsuUserStatsScreenBackgroundPPRecalculator : public Resource {
 
                     // NOTE: avoid importing the same score twice
                     if(m_bImportLegacyScores && score.isLegacyScore) {
-                        const std::vector<OsuDatabase::Score> &otherScores = (*scores)[score.md5hash];
+                        const std::vector<Score> &otherScores = (*scores)[score.md5hash];
 
                         bool isScoreAlreadyImported = false;
                         for(size_t s = 0; s < otherScores.size(); s++) {
@@ -392,7 +391,7 @@ void OsuUserStatsScreen::rebuildScoreButtons(UString playerName) {
     m_scoreButtons.clear();
 
     OsuDatabase *db = m_osu->getSongBrowser()->getDatabase();
-    std::vector<OsuDatabase::Score *> scores = db->getPlayerPPScores(playerName).ppScores;
+    std::vector<Score *> scores = db->getPlayerPPScores(playerName).ppScores;
     for(int i = scores.size() - 1; i >= std::max(0, (int)scores.size() - osu_ui_top_ranks_max.getInt()); i--) {
         const float weight = OsuDatabase::getWeightForIndex(scores.size() - 1 - i);
 
@@ -661,16 +660,15 @@ void OsuUserStatsScreen::onCopyAllScoresConfirmed(UString text, int id) {
     debugLog("Copying all scores from \"%s\" into \"%s\"\n", m_sCopyAllScoresFromUser.toUtf8(),
              playerNameToCopyInto.toUtf8());
 
-    std::unordered_map<MD5Hash, std::vector<OsuDatabase::Score>> *scores =
-        m_osu->getSongBrowser()->getDatabase()->getScores();
+    std::unordered_map<MD5Hash, std::vector<Score>> *scores = m_osu->getSongBrowser()->getDatabase()->getScores();
 
-    std::vector<OsuDatabase::Score> tempScoresToCopy;
+    std::vector<Score> tempScoresToCopy;
     for(auto &kv : *scores) {
         tempScoresToCopy.clear();
 
         // collect all to-be-copied scores for this beatmap into tempScoresToCopy
         for(size_t i = 0; i < kv.second.size(); i++) {
-            const OsuDatabase::Score &existingScore = kv.second[i];
+            const Score &existingScore = kv.second[i];
 
             // NOTE: only ever copy McOsu scores
             if(!existingScore.isLegacyScore) {
@@ -679,7 +677,7 @@ void OsuUserStatsScreen::onCopyAllScoresConfirmed(UString text, int id) {
                     // is the case
                     bool alreadyCopied = false;
                     for(size_t j = 0; j < kv.second.size(); j++) {
-                        const OsuDatabase::Score &alreadyCopiedScore = kv.second[j];
+                        const Score &alreadyCopiedScore = kv.second[j];
 
                         if(j == i) continue;
 
@@ -750,13 +748,12 @@ void OsuUserStatsScreen::onDeleteAllScoresConfirmed(UString text, int id) {
 
     debugLog("Deleting all scores for \"%s\"\n", playerName.toUtf8());
 
-    std::unordered_map<MD5Hash, std::vector<OsuDatabase::Score>> *scores =
-        m_osu->getSongBrowser()->getDatabase()->getScores();
+    std::unordered_map<MD5Hash, std::vector<Score>> *scores = m_osu->getSongBrowser()->getDatabase()->getScores();
 
     // delete every score matching the current playerName
     for(auto &kv : *scores) {
         for(size_t i = 0; i < kv.second.size(); i++) {
-            const OsuDatabase::Score &score = kv.second[i];
+            const Score &score = kv.second[i];
 
             // NOTE: only ever delete McOsu scores
             if(!score.isLegacyScore) {

+ 1 - 1
src/App/Osu/OsuVolumeOverlay.cpp

@@ -16,7 +16,7 @@
 #include "OsuPauseMenu.h"
 #include "OsuRankingScreen.h"
 #include "OsuSkin.h"
-#include "OsuSongBrowser2.h"
+#include "OsuSongBrowser.h"
 #include "OsuUIVolumeSlider.h"
 #include "OsuUserStatsScreen.h"
 #include "Sound.h"

+ 118 - 0
src/App/Osu/Score.h

@@ -0,0 +1,118 @@
+#pragma once
+#include "OsuReplay.h"
+#include "cbase.h"
+
+class OsuDatabaseBeatmap;
+
+struct Score {
+    enum class Grade {
+        XH,
+        SH,
+        X,
+        S,
+        A,
+        B,
+        C,
+        D,
+        F,
+        N  // means "no grade"
+    };
+
+    bool isLegacyScore;          // used for identifying loaded osu! scores (which don't have any custom data available)
+    bool isImportedLegacyScore;  // used for identifying imported osu! scores (which were previously legacy scores,
+                                 // so they don't have any
+                                 // numSliderBreaks/unstableRate/hitErrorAvgMin/hitErrorAvgMax)
+    int version;
+    uint64_t unixTimestamp;
+
+    uint32_t player_id = 0;
+    UString playerName;
+    bool passed = false;
+    bool ragequit = false;
+    Grade grade = Grade::N;
+    OsuDatabaseBeatmap *diff2;
+    uint64_t play_time_ms = 0;
+
+    std::string server;
+    uint64_t online_score_id = 0;
+    bool has_replay = false;
+    std::vector<OsuReplay::Frame> replay;
+    uint64_t legacyReplayTimestamp = 0;
+
+    int num300s;
+    int num100s;
+    int num50s;
+    int numGekis;
+    int numKatus;
+    int numMisses;
+
+    unsigned long long score;
+    int comboMax;
+    bool perfect;
+    int modsLegacy;
+
+    // custom
+    int numSliderBreaks;
+    float pp;
+    float unstableRate;
+    float hitErrorAvgMin;
+    float hitErrorAvgMax;
+    float starsTomTotal;
+    float starsTomAim;
+    float starsTomSpeed;
+    float speedMultiplier;
+    float CS, AR, OD, HP;
+    int maxPossibleCombo;
+    int numHitObjects;
+    int numCircles;
+    std::string experimentalModsConVars;
+
+    // runtime
+    unsigned long long sortHack;
+    MD5Hash md5hash;
+
+    bool isLegacyScoreEqualToImportedLegacyScore(const Score &importedLegacyScore) const {
+        if(!isLegacyScore) return false;
+        if(!importedLegacyScore.isImportedLegacyScore) return false;
+
+        const bool isScoreValueEqual = (score == importedLegacyScore.score);
+        const bool isTimestampEqual = (unixTimestamp == importedLegacyScore.unixTimestamp);
+        const bool isComboMaxEqual = (comboMax == importedLegacyScore.comboMax);
+        const bool isModsLegacyEqual = (modsLegacy == importedLegacyScore.modsLegacy);
+        const bool isNum300sEqual = (num300s == importedLegacyScore.num300s);
+        const bool isNum100sEqual = (num100s == importedLegacyScore.num100s);
+        const bool isNum50sEqual = (num50s == importedLegacyScore.num50s);
+        const bool isNumGekisEqual = (numGekis == importedLegacyScore.numGekis);
+        const bool isNumKatusEqual = (numKatus == importedLegacyScore.numKatus);
+        const bool isNumMissesEqual = (numMisses == importedLegacyScore.numMisses);
+
+        return (isScoreValueEqual && isTimestampEqual && isComboMaxEqual && isModsLegacyEqual && isNum300sEqual &&
+                isNum100sEqual && isNum50sEqual && isNumGekisEqual && isNumKatusEqual && isNumMissesEqual);
+    }
+
+    bool isScoreEqualToCopiedScoreIgnoringPlayerName(const Score &copiedScore) const {
+        const bool isScoreValueEqual = (score == copiedScore.score);
+        const bool isTimestampEqual = (unixTimestamp == copiedScore.unixTimestamp);
+        const bool isComboMaxEqual = (comboMax == copiedScore.comboMax);
+        const bool isModsLegacyEqual = (modsLegacy == copiedScore.modsLegacy);
+        const bool isNum300sEqual = (num300s == copiedScore.num300s);
+        const bool isNum100sEqual = (num100s == copiedScore.num100s);
+        const bool isNum50sEqual = (num50s == copiedScore.num50s);
+        const bool isNumGekisEqual = (numGekis == copiedScore.numGekis);
+        const bool isNumKatusEqual = (numKatus == copiedScore.numKatus);
+        const bool isNumMissesEqual = (numMisses == copiedScore.numMisses);
+
+        const bool isSpeedMultiplierEqual = (speedMultiplier == copiedScore.speedMultiplier);
+        const bool isCSEqual = (CS == copiedScore.CS);
+        const bool isAREqual = (AR == copiedScore.AR);
+        const bool isODEqual = (OD == copiedScore.OD);
+        const bool isHPEqual = (HP == copiedScore.HP);
+        const bool areExperimentalModsConVarsEqual = (experimentalModsConVars == copiedScore.experimentalModsConVars);
+
+        return (isScoreValueEqual && isTimestampEqual && isComboMaxEqual && isModsLegacyEqual && isNum300sEqual &&
+                isNum100sEqual && isNum50sEqual && isNumGekisEqual && isNumKatusEqual && isNumMissesEqual
+
+                && isSpeedMultiplierEqual && isCSEqual && isAREqual && isODEqual && isHPEqual &&
+                areExperimentalModsConVarsEqual);
+    }
+};

+ 3 - 0
src/Engine/Engine.cpp

@@ -257,6 +257,9 @@ void Engine::loadApp() {
     if(!env->directoryExists(MCENGINE_DATA_DIR "maps")) {
         env->createDirectory(MCENGINE_DATA_DIR "maps");
     }
+    if(!env->directoryExists(MCENGINE_DATA_DIR "replays")) {
+        env->createDirectory(MCENGINE_DATA_DIR "replays");
+    }
     if(!env->directoryExists(MCENGINE_DATA_DIR "screenshots")) {
         env->createDirectory(MCENGINE_DATA_DIR "screenshots");
     }