Clément Wolf 2 veckor sedan
förälder
incheckning
9fbc8d56a4

+ 11 - 1
src/App/Osu/Collections.cpp

@@ -175,7 +175,17 @@ bool load_collections() {
     }
     free(neosu_collections.memory);
 
-    debugLog("collections.db: loading took %f seconds\n", (engine->getTimeReal() - startTime));
+    u32 nb_peppy = 0;
+    u32 nb_neosu = 0;
+    u32 nb_total = 0;
+    for(auto collection : collections) {
+        nb_peppy += collection->peppy_maps.size();
+        nb_neosu += collection->neosu_maps.size();
+        nb_total += collection->maps.size();
+    }
+
+    debugLog("peppy+neosu collections: loading took %f seconds (%d peppy, %d neosu, %d maps total)\n",
+             (engine->getTimeReal() - startTime), nb_peppy, nb_neosu, nb_total);
     collections_loaded = true;
     return true;
 }

+ 161 - 183
src/App/Osu/Database.cpp

@@ -48,7 +48,6 @@ ConVar osu_database_version("osu_database_version", OSU_VERSION_DATEONLY, FCVAR_
 ConVar osu_database_ignore_version_warnings("osu_database_ignore_version_warnings", false, FCVAR_NONE);
 ConVar osu_database_ignore_version("osu_database_ignore_version", true, FCVAR_NONE,
                                    "ignore upper version limit and force load the db file (may crash)");
-ConVar osu_database_stars_cache_enabled("osu_database_stars_cache_enabled", false, FCVAR_NONE);
 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");
@@ -985,7 +984,7 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
     struct BeatmapSet {
         int setID;
         std::string path;
-        std::vector<DatabaseBeatmap *> diffs2;
+        std::vector<DatabaseBeatmap *> *diffs2 = nullptr;
     };
     std::vector<BeatmapSet> beatmapSets;
     std::unordered_map<int, size_t> setIDToIndex;
@@ -1016,8 +1015,7 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
         std::string difficultyName = read_stdstring(db);
         trim(&difficultyName);
         std::string audioFileName = read_stdstring(db);
-        auto hash_str = read_stdstring(db);
-        MD5Hash md5hash = hash_str.c_str();
+        auto md5hash = read_hash(db);
         std::string osuFileName = read_stdstring(db);
         /*unsigned char rankedStatus = */ read<u8>(db);
         unsigned short numCircles = read<u16>(db);
@@ -1042,14 +1040,6 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
 
             if(mods == 0) numOsuStandardStars = starRating;
         }
-        // NOTE: if we have our own stars cached then prefer that
-        {
-            if(osu_database_stars_cache_enabled.getBool())
-                numOsuStandardStars = 0.0f;  // NOTE: force don't use stable stars
-
-            const auto result = m_starsCache.find(md5hash);
-            if(result != m_starsCache.end()) numOsuStandardStars = result->second.starsNomod;
-        }
 
         unsigned int numTaikoStarRatings = read<u32>(db);
         // debugLog("%i star ratings for taiko\n", numTaikoStarRatings);
@@ -1166,98 +1156,114 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
             continue;
 
         // fill diff with data
-        if(mode == 0) {
-            DatabaseBeatmap *diff2 = new DatabaseBeatmap(m_osu, fullFilePath, beatmapPath);
-            {
-                diff2->m_sTitle = songTitle;
-                diff2->m_sAudioFileName = audioFileName;
-                diff2->m_iLengthMS = duration;
-
-                diff2->m_fStackLeniency = stackLeniency;
-
-                diff2->m_sArtist = artistName;
-                diff2->m_sCreator = creatorName;
-                diff2->m_sDifficultyName = difficultyName;
-                diff2->m_sSource = songSource;
-                diff2->m_sTags = songTags;
-                diff2->m_sMD5Hash = md5hash;
-                diff2->m_iID = beatmapID;
-                diff2->m_iSetID = beatmapSetID;
-
-                diff2->m_fAR = AR;
-                diff2->m_fCS = CS;
-                diff2->m_fHP = HP;
-                diff2->m_fOD = OD;
-                diff2->m_fSliderMultiplier = sliderMultiplier;
-
-                // diff2->m_sBackgroundImageFileName = "";
-
-                diff2->m_iPreviewTime = previewTime;
-                diff2->last_modification_time = lastModificationTime;
-
-                diff2->m_sFullSoundFilePath = beatmapPath;
-                diff2->m_sFullSoundFilePath.append(diff2->m_sAudioFileName);
-                diff2->m_iLocalOffset = localOffset;
-                diff2->m_iOnlineOffset = (long)onlineOffset;
-                diff2->m_iNumObjects = numCircles + numSliders + numSpinners;
-                diff2->m_iNumCircles = numCircles;
-                diff2->m_iNumSliders = numSliders;
-                diff2->m_iNumSpinners = numSpinners;
-                diff2->m_fStarsNomod = numOsuStandardStars;
-
-                // calculate bpm range
-                auto bpm = getBPM(timingPoints, (numTimingPoints > 0 ? timingPoints[numTimingPoints - 1].offset : 0));
+        if(mode != 0) continue;
+
+        DatabaseBeatmap *diff2 = new DatabaseBeatmap(m_osu, fullFilePath, beatmapPath);
+        {
+            diff2->m_sTitle = songTitle;
+            diff2->m_sAudioFileName = audioFileName;
+            diff2->m_iLengthMS = duration;
+
+            diff2->m_fStackLeniency = stackLeniency;
+
+            diff2->m_sArtist = artistName;
+            diff2->m_sCreator = creatorName;
+            diff2->m_sDifficultyName = difficultyName;
+            diff2->m_sSource = songSource;
+            diff2->m_sTags = songTags;
+            diff2->m_sMD5Hash = md5hash;
+            diff2->m_iID = beatmapID;
+            diff2->m_iSetID = beatmapSetID;
+
+            diff2->m_fAR = AR;
+            diff2->m_fCS = CS;
+            diff2->m_fHP = HP;
+            diff2->m_fOD = OD;
+            diff2->m_fSliderMultiplier = sliderMultiplier;
+
+            // diff2->m_sBackgroundImageFileName = "";
+
+            diff2->m_iPreviewTime = previewTime;
+            diff2->last_modification_time = lastModificationTime;
+
+            diff2->m_sFullSoundFilePath = beatmapPath;
+            diff2->m_sFullSoundFilePath.append(diff2->m_sAudioFileName);
+            diff2->m_iLocalOffset = localOffset;
+            diff2->m_iOnlineOffset = (long)onlineOffset;
+            diff2->m_iNumObjects = numCircles + numSliders + numSpinners;
+            diff2->m_iNumCircles = numCircles;
+            diff2->m_iNumSliders = numSliders;
+            diff2->m_iNumSpinners = numSpinners;
+
+            // NOTE: if we have our own stars/bpm cached then use that
+            bool bpm_was_cached = false;
+            diff2->m_fStarsNomod = numOsuStandardStars;
+
+            const auto result = m_starsCache.find(md5hash);
+            if(result != m_starsCache.end()) {
+                if(result->second.starsNomod >= 0.f) {
+                    diff2->m_fStarsNomod = result->second.starsNomod;
+                }
+                if(result->second.min_bpm >= 0) {
+                    diff2->m_iMinBPM = result->second.min_bpm;
+                    diff2->m_iMaxBPM = result->second.max_bpm;
+                    diff2->m_iMostCommonBPM = result->second.common_bpm;
+                    bpm_was_cached = true;
+                }
+            }
+
+            if(!bpm_was_cached) {
+                auto bpm = getBPM(timingPoints);
                 diff2->m_iMinBPM = bpm.min;
                 diff2->m_iMaxBPM = bpm.max;
                 diff2->m_iMostCommonBPM = bpm.most_common;
+            }
 
-                // build temp partial timingpoints, only used for menu animations
-                // a bit hacky to avoid slow ass allocations
-                diff2->m_timingpoints.resize(numTimingPoints);
-                memset(diff2->m_timingpoints.data(), 0, numTimingPoints * sizeof(DatabaseBeatmap::TIMINGPOINT));
-                for(int t = 0; t < numTimingPoints; t++) {
-                    diff2->m_timingpoints[t].offset = (long)timingPoints[t].offset;
-                    diff2->m_timingpoints[t].msPerBeat = (float)timingPoints[t].msPerBeat;
-                    diff2->m_timingpoints[t].timingChange = timingPoints[t].timingChange;
-                }
+            // build temp partial timingpoints, only used for menu animations
+            // a bit hacky to avoid slow ass allocations
+            diff2->m_timingpoints.resize(numTimingPoints);
+            memset(diff2->m_timingpoints.data(), 0, numTimingPoints * sizeof(DatabaseBeatmap::TIMINGPOINT));
+            for(int t = 0; t < numTimingPoints; t++) {
+                diff2->m_timingpoints[t].offset = (long)timingPoints[t].offset;
+                diff2->m_timingpoints[t].msPerBeat = (float)timingPoints[t].msPerBeat;
+                diff2->m_timingpoints[t].timingChange = timingPoints[t].timingChange;
             }
+        }
 
-            // special case: legacy fallback behavior for invalid beatmapSetID, try to parse the ID from the path
-            if(beatmapSetID < 1 && path.length() > 0) {
-                auto upath = UString(path.c_str());
-                const std::vector<UString> pathTokens =
-                    upath.split("\\");  // NOTE: this is hardcoded to backslash since osu is windows only
-                if(pathTokens.size() > 0 && pathTokens[0].length() > 0) {
-                    const std::vector<UString> spaceTokens = pathTokens[0].split(" ");
-                    if(spaceTokens.size() > 0 && spaceTokens[0].length() > 0) {
-                        try {
-                            beatmapSetID = spaceTokens[0].toInt();
-                        } catch(...) {
-                            beatmapSetID = -1;
-                        }
+        // special case: legacy fallback behavior for invalid beatmapSetID, try to parse the ID from the path
+        if(beatmapSetID < 1 && path.length() > 0) {
+            auto upath = UString(path.c_str());
+            const std::vector<UString> pathTokens =
+                upath.split("\\");  // NOTE: this is hardcoded to backslash since osu is windows only
+            if(pathTokens.size() > 0 && pathTokens[0].length() > 0) {
+                const std::vector<UString> spaceTokens = pathTokens[0].split(" ");
+                if(spaceTokens.size() > 0 && spaceTokens[0].length() > 0) {
+                    try {
+                        beatmapSetID = spaceTokens[0].toInt();
+                    } catch(...) {
+                        beatmapSetID = -1;
                     }
                 }
             }
+        }
 
-            // (the diff is now fully built)
-
-            // now, search if the current set (to which this diff would belong) already exists and add it there, or if
-            // it doesn't exist then create the set
-            const auto result = setIDToIndex.find(beatmapSetID);
-            const bool beatmapSetExists = (result != setIDToIndex.end());
-            if(beatmapSetExists)
-                beatmapSets[result->second].diffs2.push_back(diff2);
-            else {
-                setIDToIndex[beatmapSetID] = beatmapSets.size();
-
-                BeatmapSet s;
-
-                s.setID = beatmapSetID;
-                s.path = beatmapPath;
-                s.diffs2.push_back(diff2);
-
-                beatmapSets.push_back(s);
-            }
+        // (the diff is now fully built)
+
+        // now, search if the current set (to which this diff would belong) already exists and add it there, or if
+        // it doesn't exist then create the set
+        const auto result = setIDToIndex.find(beatmapSetID);
+        const bool beatmapSetExists = (result != setIDToIndex.end());
+        if(beatmapSetExists) {
+            beatmapSets[result->second].diffs2->push_back(diff2);
+        } else {
+            setIDToIndex[beatmapSetID] = beatmapSets.size();
+
+            BeatmapSet s;
+            s.setID = beatmapSetID;
+            s.path = beatmapPath;
+            s.diffs2 = new std::vector<DatabaseBeatmap *>();
+            s.diffs2->push_back(diff2);
+            beatmapSets.push_back(s);
         }
     }
 
@@ -1268,10 +1274,10 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
     for(int i = 0; i < beatmapSets.size(); i++) {
         if(m_bInterruptLoad.load()) break;  // cancellation point
 
-        if(beatmapSets[i].diffs2.size() > 0)  // sanity check
+        if(!beatmapSets[i].diffs2->empty())  // sanity check
         {
             if(beatmapSets[i].setID > 0) {
-                DatabaseBeatmap *bm = new DatabaseBeatmap(m_osu, std::move(beatmapSets[i].diffs2));
+                DatabaseBeatmap *bm = new DatabaseBeatmap(m_osu, beatmapSets[i].diffs2);
 
                 m_databaseBeatmaps.push_back(bm);
 
@@ -1290,13 +1296,13 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
     for(int i = 0; i < beatmapSets.size(); i++) {
         if(m_bInterruptLoad.load()) break;  // cancellation point
 
-        if(beatmapSets[i].diffs2.size() > 0)  // sanity check
+        if(!beatmapSets[i].diffs2->empty())  // sanity check
         {
             if(beatmapSets[i].setID < 1) {
-                for(int b = 0; b < beatmapSets[i].diffs2.size(); b++) {
+                for(int b = 0; b < beatmapSets[i].diffs2->size(); b++) {
                     if(m_bInterruptLoad.load()) break;  // cancellation point
 
-                    DatabaseBeatmap *diff2 = beatmapSets[i].diffs2[b];
+                    DatabaseBeatmap *diff2 = (*beatmapSets[i].diffs2)[b];
 
                     // try finding an already existing beatmap with matching artist and title and creator (into which we
                     // could inject this lone diff)
@@ -1319,10 +1325,10 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
 
                     // if we couldn't find any beatmap with our title and artist, create a new one
                     if(!existsAlready) {
-                        std::vector<DatabaseBeatmap *> diffs2;
-                        diffs2.push_back(beatmapSets[i].diffs2[b]);
+                        auto diffs2 = new std::vector<DatabaseBeatmap *>();
+                        diffs2->push_back((*beatmapSets[i].diffs2)[b]);
 
-                        DatabaseBeatmap *bm = new DatabaseBeatmap(m_osu, std::move(diffs2));
+                        DatabaseBeatmap *bm = new DatabaseBeatmap(m_osu, diffs2);
 
                         m_databaseBeatmaps.push_back(bm);
                     }
@@ -1331,87 +1337,88 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
         }
     }
 
+    load_collections();
+
     m_importTimer->update();
     debugLog("Refresh finished, added %i beatmaps in %f seconds.\n", m_databaseBeatmaps.size(),
              m_importTimer->getElapsedTime());
 
-    load_collections();
-
     // signal that we are done
     m_fLoadingProgress = 1.0f;
 }
 
 void Database::loadStars() {
-    if(!osu_database_stars_cache_enabled.getBool()) return;
+    Packet cache = load_db("stars.cache");
+    if(cache.size <= 0) return;
+
+    m_starsCache.clear();
 
-    debugLog("Database::loadStars()\n");
+    const int cacheVersion = read<u32>(&cache);
+    if(cacheVersion > STARS_CACHE_VERSION) {
+        debugLog("Invalid stars cache version, ignoring.\n");
+        free(cache.memory);
+        return;
+    }
 
-    const int starsCacheVersion = 20221108;
+    skip_string(&cache);  // ignore md5
+    const i64 numStarsCacheEntries = read<u64>(&cache);
 
-    Packet cache = load_db("stars.cache");
-    if(cache.size > 0) {
-        m_starsCache.clear();
+    debugLog("Stars cache: version = %i, numStarsCacheEntries = %i\n", cacheVersion, numStarsCacheEntries);
 
-        const int cacheVersion = read<u32>(&cache);
+    for(i64 i = 0; i < numStarsCacheEntries; i++) {
+        auto beatmapMD5Hash = read_hash(&cache);
 
-        if(cacheVersion <= starsCacheVersion) {
-            skip_string(&cache);  // ignore md5
-            const i64 numStarsCacheEntries = read<u64>(&cache);
+        STARS_CACHE_ENTRY entry;
+        entry.starsNomod = read<f32>(&cache);
 
-            debugLog("Stars cache: version = %i, numStarsCacheEntries = %i\n", cacheVersion, numStarsCacheEntries);
+        if(cacheVersion >= 20240430) {
+            entry.min_bpm = read<i32>(&cache);
+            entry.max_bpm = read<i32>(&cache);
+            entry.common_bpm = read<i32>(&cache);
+        }
 
-            for(i64 i = 0; i < numStarsCacheEntries; i++) {
-                auto hash_str = read_stdstring(&cache);
-                const MD5Hash beatmapMD5Hash = hash_str.c_str();
-                const float starsNomod = read<f32>(&cache);
+        m_starsCache[beatmapMD5Hash] = entry;
+    }
 
-                STARS_CACHE_ENTRY entry;
-                { entry.starsNomod = starsNomod; }
-                m_starsCache[beatmapMD5Hash] = entry;
-            }
-        } else
-            debugLog("Invalid stars cache version, ignoring.\n");
-    } else
-        debugLog("No stars cache found.\n");
+    free(cache.memory);
 }
 
 void Database::saveStars() {
-    if(!osu_database_stars_cache_enabled.getBool()) return;
-
     debugLog("Osu: Saving stars ...\n");
 
-    const int starsCacheVersion = 20221108;
-
-    // count
     i64 numStarsCacheEntries = 0;
     for(DatabaseBeatmap *beatmap : m_databaseBeatmaps) {
-        for(DatabaseBeatmap *diff2 : beatmap->getDifficulties()) {
-            if(diff2->getStarsNomod() > 0.0f && diff2->getStarsNomod() != 0.0001f) numStarsCacheEntries++;
-        }
+        numStarsCacheEntries += beatmap->getDifficulties().size();
     }
 
-    if(numStarsCacheEntries < 1) {
-        debugLog("No stars cached, nothing to write.\n");
+    // 100 instead of 1 because player can idle in main menu while shuffling without loading database
+    // If the player *did* load the database and actually has less than 100 beatmaps, then it doesn't matter
+    // since getting the stars/bpm/etc would be fast enough anyway.
+    if(numStarsCacheEntries < 100) {
+        debugLog("No beatmaps loaded, nothing to write.\n");
         return;
     }
 
     // write
     Packet cache;
-    write_u32(&cache, starsCacheVersion);
+    write_u32(&cache, STARS_CACHE_VERSION);
     write_string(&cache, "00000000000000000000000000000000");
     write_u64(&cache, numStarsCacheEntries);
     for(DatabaseBeatmap *beatmap : m_databaseBeatmaps) {
         for(DatabaseBeatmap *diff2 : beatmap->getDifficulties()) {
-            if(diff2->getStarsNomod() > 0.0f && diff2->getStarsNomod() != 0.0001f) {
-                write_string(&cache, diff2->getMD5Hash().hash);
-                write_f32(&cache, diff2->getStarsNomod());
-            }
+            write_string(&cache, diff2->getMD5Hash().hash);
+            write_f32(&cache, diff2->getStarsNomod());
+            write<i32>(&cache, diff2->getMinBPM());
+            write<i32>(&cache, diff2->getMaxBPM());
+            write<i32>(&cache, diff2->getMostCommonBPM());
         }
     }
 
     if(!save_db(&cache, "stars.cache")) {
         debugLog("Couldn't write stars.cache!\n");
     }
+
+    free(cache.memory);
 }
 
 void Database::loadScores() {
@@ -1441,24 +1448,13 @@ void Database::loadScores() {
 
             if(dbVersion <= LiveScore::VERSION) {
                 for(int b = 0; b < numBeatmaps; b++) {
-                    auto hash_str = read_stdstring(&db);
+                    auto md5hash = read_hash(&db);
                     const int numScores = read<u32>(&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(Osu::debug->getBool()) {
-                        debugLog("Beatmap[%i]: md5hash = %s, numScores = %i\n", b, hash_str.c_str(), numScores);
+                        debugLog("Beatmap[%i]: md5hash = %s, numScores = %i\n", b, md5hash.hash, numScores);
                     }
 
-                    const MD5Hash md5hash = hash_str.c_str();
                     for(int s = 0; s < numScores; s++) {
                         FinishedScore sc;
                         sc.isLegacyScore = false;
@@ -1561,19 +1557,7 @@ void Database::loadScores() {
             debugLog("Legacy scores: version = %i, numBeatmaps = %i\n", dbVersion, numBeatmaps);
 
             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;
-                }
-
-                const MD5Hash md5hash = hash_str.c_str();
+                auto md5hash = read_hash(&db);
                 const int numScores = read<u32>(&db);
 
                 if(Osu::debug->getBool())
@@ -1792,7 +1776,7 @@ DatabaseBeatmap *Database::loadRawBeatmap(std::string beatmapPath) {
     if(Osu::debug->getBool()) debugLog("BeatmapDatabase::loadRawBeatmap() : %s\n", beatmapPath.c_str());
 
     // try loading all diffs
-    std::vector<DatabaseBeatmap *> diffs2;
+    std::vector<DatabaseBeatmap *> *diffs2 = new std::vector<DatabaseBeatmap *>();
     {
         std::vector<std::string> beatmapFiles = env->getFilesInFolder(beatmapPath);
         for(int i = 0; i < beatmapFiles.size(); i++) {
@@ -1806,7 +1790,10 @@ DatabaseBeatmap *Database::loadRawBeatmap(std::string beatmapPath) {
                 DatabaseBeatmap *diff2 = new DatabaseBeatmap(m_osu, fullFilePath, beatmapPath);
 
                 // try to load it. if successful save it, else cleanup and continue to the next osu file
-                if(!DatabaseBeatmap::loadMetadata(diff2)) {
+                if(DatabaseBeatmap::loadMetadata(diff2)) {
+                    // (metadata loaded successfully)
+                    diffs2->push_back(diff2);
+                } else {
                     if(Osu::debug->getBool()) {
                         debugLog("BeatmapDatabase::loadRawBeatmap() : Couldn't loadMetadata(), deleting object.\n");
                         if(diff2->getGameMode() == 0)
@@ -1814,26 +1801,17 @@ DatabaseBeatmap *Database::loadRawBeatmap(std::string beatmapPath) {
                                                        "Couldn't loadMetadata()\n");
                     }
                     SAFE_DELETE(diff2);
-                    continue;
                 }
-
-                // (metadata loaded successfully)
-
-                // NOTE: if we have our own stars cached then use that
-                {
-                    const auto result = m_starsCache.find(diff2->getMD5Hash());
-                    if(result != m_starsCache.end()) diff2->m_fStarsNomod = result->second.starsNomod;
-                }
-
-                diffs2.push_back(diff2);
             }
         }
     }
 
     // build beatmap from diffs
     DatabaseBeatmap *beatmap = NULL;
-    if(diffs2.size() > 0) {
-        beatmap = new DatabaseBeatmap(m_osu, std::move(diffs2));
+    if(diffs2->empty()) {
+        delete diffs2;
+    } else {
+        beatmap = new DatabaseBeatmap(m_osu, diffs2);
     }
 
     return beatmap;

+ 11 - 6
src/App/Osu/Database.h

@@ -13,6 +13,8 @@ class OsuFile;
 class DatabaseBeatmap;
 class DatabaseLoader;
 
+#define STARS_CACHE_VERSION 20240430
+
 // Field ordering matters here
 #pragma pack(push, 1)
 struct TIMINGPOINT {
@@ -99,6 +101,15 @@ class Database {
 
     void loadDB(Packet *db, bool &fallbackToRawLoad);
 
+    // stars.cache
+    struct STARS_CACHE_ENTRY {
+        float starsNomod = -1;
+        i32 min_bpm = -1;
+        i32 max_bpm = -1;
+        i32 common_bpm = -1;
+    };
+    std::unordered_map<MD5Hash, STARS_CACHE_ENTRY> m_starsCache;
+
    private:
     friend class DatabaseLoader;
 
@@ -149,10 +160,4 @@ class Database {
     std::string m_sRawBeatmapLoadOsuSongFolder;
     std::vector<std::string> m_rawBeatmapFolders;
     std::vector<std::string> m_rawLoadBeatmapFolders;
-
-    // stars.cache
-    struct STARS_CACHE_ENTRY {
-        float starsNomod;
-    };
-    std::unordered_map<MD5Hash, STARS_CACHE_ENTRY> m_starsCache;
 };

+ 43 - 19
src/App/Osu/DatabaseBeatmap.cpp

@@ -14,6 +14,7 @@
 #include "Beatmap.h"
 #include "Circle.h"
 #include "ConVar.h"
+#include "Database.h"
 #include "Engine.h"
 #include "File.h"
 #include "GameRules.h"
@@ -23,6 +24,7 @@
 #include "Skin.h"
 #include "Slider.h"
 #include "SliderCurves.h"
+#include "SongBrowser.h"
 #include "Spinner.h"
 
 ConVar osu_mod_random("osu_mod_random", false, FCVAR_NONVANILLA);
@@ -82,6 +84,7 @@ ConVar *DatabaseBeatmap::m_osu_debug_pp_ref = NULL;
 ConVar *DatabaseBeatmap::m_osu_slider_end_inside_check_offset_ref = NULL;
 
 DatabaseBeatmap::DatabaseBeatmap(Osu *osu, std::string filePath, std::string folder, bool filePathIsInMemoryBeatmap) {
+    m_difficulties = new std::vector<DatabaseBeatmap *>();
     m_osu = osu;
 
     m_sFilePath = filePath;
@@ -141,15 +144,16 @@ DatabaseBeatmap::DatabaseBeatmap(Osu *osu, std::string filePath, std::string fol
     m_iOnlineOffset = 0;
 }
 
-DatabaseBeatmap::DatabaseBeatmap(Osu *osu, std::vector<DatabaseBeatmap *> &&difficulties)
+DatabaseBeatmap::DatabaseBeatmap(Osu *osu, std::vector<DatabaseBeatmap *> *difficulties)
     : DatabaseBeatmap(osu, "", "") {
-    setDifficulties(std::move(difficulties));
+    setDifficulties(difficulties);
 }
 
 DatabaseBeatmap::~DatabaseBeatmap() {
-    for(size_t i = 0; i < m_difficulties.size(); i++) {
-        delete m_difficulties[i];
+    for(size_t i = 0; i < m_difficulties->size(); i++) {
+        delete(*m_difficulties)[i];
     }
+    SAFE_DELETE(m_difficulties);
 }
 
 DatabaseBeatmap::PRIMITIVE_CONTAINER DatabaseBeatmap::loadPrimitiveObjects(const std::string &osuFilePath,
@@ -939,7 +943,7 @@ DatabaseBeatmap::LOAD_DIFFOBJ_RESULT DatabaseBeatmap::loadDifficultyHitObjects(c
 
 bool DatabaseBeatmap::loadMetadata(DatabaseBeatmap *databaseBeatmap) {
     if(databaseBeatmap == NULL) return false;
-    if(databaseBeatmap->m_difficulties.size() > 0) return false;  // we are just a container
+    if(!databaseBeatmap->m_difficulties->empty()) return false;  // we are just a container
 
     // reset
     databaseBeatmap->m_timingpoints.clear();
@@ -1181,12 +1185,28 @@ bool DatabaseBeatmap::loadMetadata(DatabaseBeatmap *databaseBeatmap) {
         std::sort(databaseBeatmap->m_timingpoints.begin(), databaseBeatmap->m_timingpoints.end(),
                   TimingPointSortComparator());
 
-        // calculate bpm range
-        auto bpm = getBPM(databaseBeatmap->m_timingpoints,
-                          databaseBeatmap->m_timingpoints[databaseBeatmap->m_timingpoints.size() - 1].offset);
-        databaseBeatmap->m_iMinBPM = bpm.min;
-        databaseBeatmap->m_iMaxBPM = bpm.max;
-        databaseBeatmap->m_iMostCommonBPM = bpm.most_common;
+        // NOTE: if we have our own stars/bpm cached then use that
+        bool bpm_was_cached = false;
+        auto db = bancho.osu->getSongBrowser()->getDatabase();
+        const auto result = db->m_starsCache.find(databaseBeatmap->getMD5Hash());
+        if(result != db->m_starsCache.end()) {
+            if(result->second.starsNomod >= 0.f) {
+                databaseBeatmap->m_fStarsNomod = result->second.starsNomod;
+            }
+            if(result->second.min_bpm >= 0) {
+                databaseBeatmap->m_iMinBPM = result->second.min_bpm;
+                databaseBeatmap->m_iMaxBPM = result->second.max_bpm;
+                databaseBeatmap->m_iMostCommonBPM = result->second.common_bpm;
+                bpm_was_cached = true;
+            }
+        }
+
+        if(!bpm_was_cached) {
+            auto bpm = getBPM(databaseBeatmap->m_timingpoints);
+            databaseBeatmap->m_iMinBPM = bpm.min;
+            databaseBeatmap->m_iMaxBPM = bpm.max;
+            databaseBeatmap->m_iMostCommonBPM = bpm.most_common;
+        }
     }
 
     // special case: old beatmaps have AR = OD, there is no ApproachRate stored
@@ -1466,15 +1486,19 @@ DatabaseBeatmap::LOAD_GAMEPLAY_RESULT DatabaseBeatmap::loadGameplay(DatabaseBeat
     return result;
 }
 
-void DatabaseBeatmap::setDifficulties(std::vector<DatabaseBeatmap *> &&difficulties) {
-    m_difficulties = std::move(difficulties);
-    if(m_difficulties.empty()) return;
+void DatabaseBeatmap::setDifficulties(std::vector<DatabaseBeatmap *> *difficulties) {
+    if(m_difficulties != difficulties) {
+        delete m_difficulties;
+        m_difficulties = difficulties;
+    }
+
+    if(m_difficulties->empty()) return;
 
     // set representative values for this container (i.e. use values from first difficulty)
-    m_sTitle = m_difficulties[0]->m_sTitle;
-    m_sArtist = m_difficulties[0]->m_sArtist;
-    m_sCreator = m_difficulties[0]->m_sCreator;
-    m_sBackgroundImageFileName = m_difficulties[0]->m_sBackgroundImageFileName;
+    m_sTitle = (*m_difficulties)[0]->m_sTitle;
+    m_sArtist = (*m_difficulties)[0]->m_sArtist;
+    m_sCreator = (*m_difficulties)[0]->m_sCreator;
+    m_sBackgroundImageFileName = (*m_difficulties)[0]->m_sBackgroundImageFileName;
 
     // also precalculate some largest representative values
     m_iLengthMS = 0;
@@ -1487,7 +1511,7 @@ void DatabaseBeatmap::setDifficulties(std::vector<DatabaseBeatmap *> &&difficult
     m_iMaxBPM = 0;
     m_iMostCommonBPM = 0;
     last_modification_time = 0;
-    for(auto diff : m_difficulties) {
+    for(auto diff : (*m_difficulties)) {
         if(diff->getLengthMS() > m_iLengthMS) m_iLengthMS = diff->getLengthMS();
         if(diff->getCS() > m_fCS) m_fCS = diff->getCS();
         if(diff->getAR() > m_fAR) m_fAR = diff->getAR();

+ 23 - 22
src/App/Osu/DatabaseBeatmap.h

@@ -87,7 +87,7 @@ class DatabaseBeatmap {
     };
 
     DatabaseBeatmap(Osu *osu, std::string filePath, std::string folder, bool filePathIsInMemoryBeatmap = false);
-    DatabaseBeatmap(Osu *osu, std::vector<DatabaseBeatmap *> &&difficulties);
+    DatabaseBeatmap(Osu *osu, std::vector<DatabaseBeatmap *> *difficulties);
     ~DatabaseBeatmap();
 
     static LOAD_DIFFOBJ_RESULT loadDifficultyHitObjects(const std::string &osuFilePath, float AR, float CS,
@@ -98,7 +98,7 @@ class DatabaseBeatmap {
     static bool loadMetadata(DatabaseBeatmap *databaseBeatmap);
     static LOAD_GAMEPLAY_RESULT loadGameplay(DatabaseBeatmap *databaseBeatmap, Beatmap *beatmap);
 
-    void setDifficulties(std::vector<DatabaseBeatmap *> &&difficulties);
+    void setDifficulties(std::vector<DatabaseBeatmap *> *difficulties);
 
     void setLengthMS(unsigned long lengthMS) { m_iLengthMS = lengthMS; }
 
@@ -118,7 +118,7 @@ class DatabaseBeatmap {
 
     inline unsigned long long getSortHack() const { return m_iSortHack; }
 
-    inline const std::vector<DatabaseBeatmap *> &getDifficulties() const { return m_difficulties; }
+    inline const std::vector<DatabaseBeatmap *> &getDifficulties() const { return *m_difficulties; }
 
     inline const MD5Hash &getMD5Hash() const { return m_sMD5Hash; }
 
@@ -223,9 +223,9 @@ class DatabaseBeatmap {
 
     float m_fStarsNomod;
 
-    int m_iMinBPM;
-    int m_iMaxBPM;
-    int m_iMostCommonBPM;
+    int m_iMinBPM = 0;
+    int m_iMaxBPM = 0;
+    int m_iMostCommonBPM = 0;
 
     int m_iNumObjects;
     int m_iNumCircles;
@@ -334,7 +334,7 @@ class DatabaseBeatmap {
 
     unsigned long long m_iSortHack;
 
-    std::vector<DatabaseBeatmap *> m_difficulties;
+    std::vector<DatabaseBeatmap *> *m_difficulties = nullptr;
 
     MD5Hash m_sMD5Hash;
 
@@ -434,14 +434,14 @@ class DatabaseBeatmapStarCalculator : public Resource {
 };
 
 struct BPMInfo {
-    u32 min;
-    u32 max;
-    u32 most_common;
+    i32 min;
+    i32 max;
+    i32 most_common;
 };
 
 template <typename T>
-struct BPMInfo getBPM(const zarray<T> &timing_points, long lastTime) {
-    if(timing_points.size() < 1) {
+struct BPMInfo getBPM(const zarray<T> &timing_points) {
+    if(timing_points.empty()) {
         return BPMInfo{
             .min = 0,
             .max = 0,
@@ -450,26 +450,27 @@ struct BPMInfo getBPM(const zarray<T> &timing_points, long lastTime) {
     }
 
     struct Tuple {
-        u32 bpm;
-        u32 duration;
+        i32 bpm;
+        i32 duration;
     };
 
     zarray<Tuple> bpms;
     bpms.reserve(timing_points.size());
 
+    long lastTime = timing_points[timing_points.size() - 1].offset;
     for(size_t i = 0; i < timing_points.size(); i++) {
         const T &t = timing_points[i];
-        if(t.offset > lastTime) break;
+        if(t.offset > lastTime) continue;
         if(t.msPerBeat < 0) continue;
 
         // "osu-stable forced the first control point to start at 0."
         // "This is reproduced here to maintain compatibility around osu!mania scroll speed and song
         // select display."
         const long currentTime = (i == 0 ? 0 : t.offset);
-        const long nextTime = (i >= timing_points.size() - 1 ? lastTime : timing_points[i + 1].offset);
+        const long nextTime = (i == timing_points.size() - 1 ? lastTime : timing_points[i + 1].offset);
 
-        u32 bpm = t.msPerBeat / 60000;
-        u32 duration = std::max(nextTime - currentTime, (long)0);
+        i32 bpm = t.msPerBeat / 60000;
+        i32 duration = std::max(nextTime - currentTime, (long)0);
 
         bool found = false;
         for(auto tuple : bpms) {
@@ -488,10 +489,10 @@ struct BPMInfo getBPM(const zarray<T> &timing_points, long lastTime) {
         }
     }
 
-    u32 min = 9001;
-    u32 max = 0;
-    u32 mostCommonBPM = 0;
-    u32 longestDuration = 0;
+    i32 min = 9001;
+    i32 max = 0;
+    i32 mostCommonBPM = 0;
+    i32 longestDuration = 0;
     for(auto tuple : bpms) {
         if(tuple.bpm > max) max = tuple.bpm;
         if(tuple.bpm < min) min = tuple.bpm;

+ 1 - 0
src/Util/cbase.h

@@ -183,6 +183,7 @@ struct zarray {
     void clear() { nb = 0; }
     T *begin() const { return memory; }
     T *data() { return memory; }
+    bool empty() const { return nb == 0; }
     T *end() const { return &memory[nb]; }
     size_t size() const { return nb; }