Clément Wolf пре 2 недеља
родитељ
комит
56aae671e6

+ 15 - 0
src/App/Osu/BanchoNetworking.cpp

@@ -44,6 +44,13 @@ pthread_mutex_t api_responses_mutex = PTHREAD_MUTEX_INITIALIZER;
 std::vector<APIRequest> api_request_queue;
 std::vector<Packet> api_response_queue;
 
+// dummy method to prevent curl from printing to stdout
+size_t curldummy(void *buffer, size_t size, size_t nmemb, void *userp) {
+    (void)buffer;
+    (void)userp;
+    return size * nmemb;
+}
+
 void disconnect() {
     pthread_mutex_lock(&outgoing_mutex);
 
@@ -66,6 +73,7 @@ void disconnect() {
         curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, packet.pos);
         curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
         curl_easy_setopt(curl, CURLOPT_USERAGENT, "osu!");
+        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curldummy);
 #ifdef _WIN32
         // ABSOLUTELY RETARDED, FUCK WINDOWS
         curl_easy_setopt(curl, CURLOPT_CAINFO, "curl-ca-bundle.crt");
@@ -481,6 +489,9 @@ void send_api_request(APIRequest request) {
 void send_packet(Packet &packet) {
     if(bancho.user_id <= 0) {
         // Don't queue any packets until we're logged in
+        free(packet.memory);
+        packet.memory = nullptr;
+        packet.size = 0;
         return;
     }
 
@@ -500,6 +511,10 @@ void send_packet(Packet &packet) {
     write_bytes(&outgoing, packet.memory, packet.pos);
 
     pthread_mutex_unlock(&outgoing_mutex);
+
+    free(packet.memory);
+    packet.memory = nullptr;
+    packet.size = 0;
 }
 
 void init_networking_thread() {

+ 1 - 8
src/App/Osu/BanchoProtocol.h

@@ -280,12 +280,5 @@ void write_hash(Packet *packet, MD5Hash hash);
 
 template <typename T>
 void write(Packet *packet, T t) {
-    if(packet->pos + sizeof(T) > packet->size) {
-        packet->memory = (u8 *)realloc(packet->memory, packet->size + sizeof(T) + 4096);
-        packet->size += sizeof(T) + 4096;
-        if(!packet->memory) return;
-    }
-
-    *(packet->memory + packet->pos) = t;
-    packet->pos += sizeof(T);
+    write_bytes(packet, (u8 *)&t, sizeof(T));
 }

+ 49 - 101
src/App/Osu/Database.cpp

@@ -445,10 +445,8 @@ void Database::save() {
 }
 
 DatabaseBeatmap *Database::addBeatmap(std::string beatmapFolderPath) {
-    DatabaseBeatmap *beatmap = loadRawBeatmap(beatmapFolderPath);
-
-    if(beatmap != NULL) m_databaseBeatmaps.push_back(beatmap);
-
+    BeatmapSet *beatmap = loadRawBeatmap(beatmapFolderPath);
+    if(beatmap != nullptr) m_databaseBeatmaps.push_back(beatmap);
     return beatmap;
 }
 
@@ -990,12 +988,12 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
     }
 
     // read beatmapInfos, and also build two hashmaps (diff hash -> BeatmapDifficulty, diff hash -> Beatmap)
-    struct BeatmapSet {
+    struct Beatmap_Set {
         int setID;
         std::string path;
         std::vector<DatabaseBeatmap *> *diffs2 = nullptr;
     };
-    std::vector<BeatmapSet> beatmapSets;
+    std::vector<Beatmap_Set> beatmapSets;
     std::unordered_map<int, size_t> setIDToIndex;
     for(int i = 0; i < m_iNumBeatmapsToLoad; i++) {
         if(m_bInterruptLoad.load()) break;  // cancellation point
@@ -1270,7 +1268,7 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
         } else {
             setIDToIndex[beatmapSetID] = beatmapSets.size();
 
-            BeatmapSet s;
+            Beatmap_Set s;
             s.setID = beatmapSetID;
             s.path = beatmapPath;
             s.diffs2 = new std::vector<DatabaseBeatmap *>();
@@ -1279,72 +1277,33 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
         }
     }
 
-    // we now have a collection of BeatmapSets (where one set is equal to one beatmap and all of its diffs), build the
-    // actual Beatmap objects first, build all beatmaps which have a valid setID (trusting the values from the osu
-    // database)
-    std::unordered_map<std::string, DatabaseBeatmap *> titleArtistToBeatmap;
+    // build beatmap sets
     for(int i = 0; i < beatmapSets.size(); i++) {
-        if(m_bInterruptLoad.load()) break;  // cancellation point
-
-        if(!beatmapSets[i].diffs2->empty())  // sanity check
-        {
-            if(beatmapSets[i].setID > 0) {
-                DatabaseBeatmap *bm = new DatabaseBeatmap(m_osu, beatmapSets[i].diffs2);
+        if(m_bInterruptLoad.load()) break;            // cancellation point
+        if(beatmapSets[i].diffs2->empty()) continue;  // sanity check
 
-                m_databaseBeatmaps.push_back(bm);
+        if(beatmapSets[i].setID > 0) {
+            DatabaseBeatmap *bm = new DatabaseBeatmap(m_osu, beatmapSets[i].diffs2);
+            m_databaseBeatmaps.push_back(bm);
+        } else {
+            // set with invalid ID: treat all its diffs separately. we'll group the diffs by title+artist.
+            std::unordered_map<std::string, std::vector<DatabaseBeatmap *> *> titleArtistToBeatmap;
+            for(auto diff : (*beatmapSets[i].diffs2)) {
+                std::string titleArtist = diff->getTitle();
+                titleArtist.append("|");
+                titleArtist.append(diff->getArtist());
+
+                auto it = titleArtistToBeatmap.find(titleArtist);
+                if(it == titleArtistToBeatmap.end()) {
+                    titleArtistToBeatmap[titleArtist] = new std::vector<DatabaseBeatmap *>();
+                }
 
-                // and add an entry in the hashmap
-                std::string titleArtist = bm->getTitle();
-                titleArtist.append(bm->getArtist());
-                if(titleArtist.length() > 0) titleArtistToBeatmap[titleArtist] = bm;
+                titleArtistToBeatmap[titleArtist]->push_back(diff);
             }
-        }
-    }
-
-    // second, handle all diffs which have an invalid setID, and group them exclusively by artist and title and creator
-    // (diffs with the same artist and title and creator will end up in the same beatmap object) this goes through every
-    // individual diff in a "set" (not really a set because its ID is either 0 or -1) instead of trusting the ID values
-    // from the osu database
-    for(int i = 0; i < beatmapSets.size(); i++) {
-        if(m_bInterruptLoad.load()) break;  // cancellation point
-
-        if(!beatmapSets[i].diffs2->empty())  // sanity check
-        {
-            if(beatmapSets[i].setID < 1) {
-                for(int b = 0; b < beatmapSets[i].diffs2->size(); b++) {
-                    if(m_bInterruptLoad.load()) break;  // cancellation point
-
-                    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)
-                    bool existsAlready = false;
-
-                    // new: use hashmap
-                    std::string titleArtistCreator = diff2->getTitle();
-                    titleArtistCreator.append(diff2->getArtist());
-                    titleArtistCreator.append(diff2->getCreator());
-                    if(titleArtistCreator.length() > 0) {
-                        const auto result = titleArtistToBeatmap.find(titleArtistCreator);
-                        if(result != titleArtistToBeatmap.end()) {
-                            existsAlready = true;
-
-                            // we have found a matching beatmap, add ourself to its diffs
-                            const_cast<std::vector<DatabaseBeatmap *> &>(result->second->getDifficulties())
-                                .push_back(diff2);
-                        }
-                    }
-
-                    // if we couldn't find any beatmap with our title and artist, create a new one
-                    if(!existsAlready) {
-                        auto diffs2 = new std::vector<DatabaseBeatmap *>();
-                        diffs2->push_back((*beatmapSets[i].diffs2)[b]);
 
-                        DatabaseBeatmap *bm = new DatabaseBeatmap(m_osu, diffs2);
-
-                        m_databaseBeatmaps.push_back(bm);
-                    }
-                }
+            for(auto scuffed_set : titleArtistToBeatmap) {
+                DatabaseBeatmap *bm = new DatabaseBeatmap(m_osu, scuffed_set.second);
+                m_databaseBeatmaps.push_back(bm);
             }
         }
     }
@@ -1420,8 +1379,8 @@ void Database::saveStars() {
     write<u64>(&cache, numStarsCacheEntries);
 
     cache.reserve(cache.size + (34 + 4 + 4 + 4 + 4) * numStarsCacheEntries);
-    for(DatabaseBeatmap *beatmap : m_databaseBeatmaps) {
-        for(DatabaseBeatmap *diff2 : beatmap->getDifficulties()) {
+    for(BeatmapSet *beatmap : m_databaseBeatmaps) {
+        for(BeatmapDifficulty *diff2 : beatmap->getDifficulties()) {
             write_hash(&cache, diff2->getMD5Hash().hash);
             write<f32>(&cache, diff2->getStarsNomod());
             write<i32>(&cache, diff2->getMinBPM());
@@ -1791,49 +1750,38 @@ void Database::saveScores() {
     debugLog("Took %f seconds.\n", (engine->getTimeReal() - startTime));
 }
 
-DatabaseBeatmap *Database::loadRawBeatmap(std::string beatmapPath) {
+BeatmapSet *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 = new std::vector<DatabaseBeatmap *>();
-    {
-        std::vector<std::string> beatmapFiles = env->getFilesInFolder(beatmapPath);
-        for(int i = 0; i < beatmapFiles.size(); i++) {
-            std::string ext = env->getFileExtensionFromFilePath(beatmapFiles[i]);
-
-            std::string fullFilePath = beatmapPath;
-            fullFilePath.append(beatmapFiles[i]);
-
-            // load diffs
-            if(ext.compare("osu") == 0) {
-                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)) {
-                    // (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)
-                            engine->showMessageWarning("BeatmapDatabase::loadRawBeatmap()",
-                                                       "Couldn't loadMetadata()\n");
-                    }
-                    SAFE_DELETE(diff2);
-                }
+    std::vector<BeatmapDifficulty *> *diffs2 = new std::vector<BeatmapDifficulty *>();
+    std::vector<std::string> beatmapFiles = env->getFilesInFolder(beatmapPath);
+    for(int i = 0; i < beatmapFiles.size(); i++) {
+        std::string ext = env->getFileExtensionFromFilePath(beatmapFiles[i]);
+        if(ext.compare("osu") != 0) continue;
+
+        std::string fullFilePath = beatmapPath;
+        fullFilePath.append(beatmapFiles[i]);
+
+        BeatmapDifficulty *diff2 = new BeatmapDifficulty(m_osu, fullFilePath, beatmapPath);
+        if(diff2->loadMetadata()) {
+            diffs2->push_back(diff2);
+        } else {
+            if(Osu::debug->getBool()) {
+                debugLog("BeatmapDatabase::loadRawBeatmap() : Couldn't loadMetadata(), deleting object.\n");
             }
+            SAFE_DELETE(diff2);
         }
     }
 
-    // build beatmap from diffs
-    DatabaseBeatmap *beatmap = NULL;
+    BeatmapSet *set = NULL;
     if(diffs2->empty()) {
         delete diffs2;
     } else {
-        beatmap = new DatabaseBeatmap(m_osu, diffs2);
+        set = new BeatmapSet(m_osu, diffs2);
     }
 
-    return beatmap;
+    return set;
 }
 
 void Database::onScoresRename(UString args) {

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

@@ -12,6 +12,8 @@ class Osu;
 class OsuFile;
 class DatabaseBeatmap;
 class DatabaseLoader;
+typedef DatabaseBeatmap BeatmapDifficulty;
+typedef DatabaseBeatmap BeatmapSet;
 
 #define STARS_CACHE_VERSION 20240430
 
@@ -97,7 +99,7 @@ class Database {
     std::unordered_map<MD5Hash, std::vector<FinishedScore>> m_online_scores;
     std::string getOsuSongsFolder();
 
-    DatabaseBeatmap *loadRawBeatmap(std::string beatmapPath);  // only used for raw loading without db
+    BeatmapSet *loadRawBeatmap(std::string beatmapPath);  // only used for raw loading without db
 
     void loadDB(Packet *db, bool &fallbackToRawLoad);
 

+ 220 - 235
src/App/Osu/DatabaseBeatmap.cpp

@@ -7,6 +7,8 @@
 
 #include "DatabaseBeatmap.h"
 
+#include <assert.h>
+
 #include <iostream>
 #include <sstream>
 
@@ -84,7 +86,6 @@ 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;
@@ -146,14 +147,48 @@ DatabaseBeatmap::DatabaseBeatmap(Osu *osu, std::string filePath, std::string fol
 
 DatabaseBeatmap::DatabaseBeatmap(Osu *osu, std::vector<DatabaseBeatmap *> *difficulties)
     : DatabaseBeatmap(osu, "", "") {
-    setDifficulties(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;
+
+    // also calculate largest representative values
+    m_iLengthMS = 0;
+    m_fCS = 99.f;
+    m_fAR = 0.0f;
+    m_fOD = 0.0f;
+    m_fHP = 0.0f;
+    m_fStarsNomod = 0.0f;
+    m_iMinBPM = 9001;
+    m_iMaxBPM = 0;
+    m_iMostCommonBPM = 0;
+    last_modification_time = 0;
+    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();
+        if(diff->getHP() > m_fHP) m_fHP = diff->getHP();
+        if(diff->getOD() > m_fOD) m_fOD = diff->getOD();
+        if(diff->getStarsNomod() > m_fStarsNomod) m_fStarsNomod = diff->getStarsNomod();
+        if(diff->getMinBPM() < m_iMinBPM) m_iMinBPM = diff->getMinBPM();
+        if(diff->getMaxBPM() > m_iMaxBPM) m_iMaxBPM = diff->getMaxBPM();
+        if(diff->getMostCommonBPM() > m_iMostCommonBPM) m_iMostCommonBPM = diff->getMostCommonBPM();
+        if(diff->last_modification_time > last_modification_time) last_modification_time = diff->last_modification_time;
+    }
 }
 
 DatabaseBeatmap::~DatabaseBeatmap() {
-    for(size_t i = 0; i < m_difficulties->size(); i++) {
-        delete((*m_difficulties)[i]);
+    if(m_difficulties != nullptr) {
+        for(auto diff : (*m_difficulties)) {
+            assert(diff->m_difficulties == nullptr);
+            delete diff;
+        }
+        delete m_difficulties;
     }
-    SAFE_DELETE(m_difficulties);
 }
 
 DatabaseBeatmap::PRIMITIVE_CONTAINER DatabaseBeatmap::loadPrimitiveObjects(const std::string &osuFilePath,
@@ -941,276 +976,265 @@ DatabaseBeatmap::LOAD_DIFFOBJ_RESULT DatabaseBeatmap::loadDifficultyHitObjects(c
     return result;
 }
 
-bool DatabaseBeatmap::loadMetadata(DatabaseBeatmap *databaseBeatmap) {
-    if(databaseBeatmap == NULL) return false;
-    if(!databaseBeatmap->m_difficulties->empty()) return false;  // we are just a container
+bool DatabaseBeatmap::loadMetadata() {
+    if(m_difficulties != nullptr) return false;  // we are a beatmapset, not a difficulty
 
     // reset
-    databaseBeatmap->m_timingpoints.clear();
+    m_timingpoints.clear();
 
-    if(Osu::debug->getBool()) debugLog("DatabaseBeatmap::loadMetadata() : %s\n", databaseBeatmap->m_sFilePath.c_str());
+    if(Osu::debug->getBool()) debugLog("DatabaseBeatmap::loadMetadata() : %s\n", m_sFilePath.c_str());
 
     // generate MD5 hash (loads entire file, very slow)
     {
-        File file(!databaseBeatmap->m_bFilePathIsInMemoryBeatmap ? databaseBeatmap->m_sFilePath : "");
+        File file(!m_bFilePathIsInMemoryBeatmap ? m_sFilePath : "");
 
         const u8 *beatmapFile = NULL;
         size_t beatmapFileSize = 0;
         {
-            if(!databaseBeatmap->m_bFilePathIsInMemoryBeatmap) {
+            if(!m_bFilePathIsInMemoryBeatmap) {
                 if(file.canRead()) {
                     beatmapFile = file.readFile();
                     beatmapFileSize = file.getFileSize();
                 }
             } else {
-                beatmapFile = (u8 *)databaseBeatmap->m_sFilePath.c_str();
-                beatmapFileSize = databaseBeatmap->m_sFilePath.size();
+                beatmapFile = (u8 *)m_sFilePath.c_str();
+                beatmapFileSize = m_sFilePath.size();
             }
         }
 
         if(beatmapFile != NULL) {
             auto hash = md5((u8 *)beatmapFile, beatmapFileSize);
-            databaseBeatmap->m_sMD5Hash = MD5Hash(hash.toUtf8());
+            m_sMD5Hash = MD5Hash(hash.toUtf8());
         }
     }
 
     // open osu file again, but this time for parsing
     bool foundAR = false;
-    {
-        File file(!databaseBeatmap->m_bFilePathIsInMemoryBeatmap ? databaseBeatmap->m_sFilePath : "");
-        if(!file.canRead() && !databaseBeatmap->m_bFilePathIsInMemoryBeatmap) {
-            debugLog("Osu Error: Couldn't read file %s\n", databaseBeatmap->m_sFilePath.c_str());
-            return false;
-        }
 
-        std::istringstream ss(databaseBeatmap->m_bFilePathIsInMemoryBeatmap ? databaseBeatmap->m_sFilePath.c_str()
-                                                                            : "");  // eh
+    File file(!m_bFilePathIsInMemoryBeatmap ? m_sFilePath : "");
+    if(!file.canRead() && !m_bFilePathIsInMemoryBeatmap) {
+        debugLog("Osu Error: Couldn't read file %s\n", m_sFilePath.c_str());
+        return false;
+    }
 
-        // load metadata only
-        int curBlock = -1;
-        unsigned long long timingPointSortHack = 0;
-        char stringBuffer[1024];
-        std::string curLine;
-        while(!databaseBeatmap->m_bFilePathIsInMemoryBeatmap ? file.canRead()
-                                                             : static_cast<bool>(std::getline(ss, curLine))) {
-            if(!databaseBeatmap->m_bFilePathIsInMemoryBeatmap) {
-                curLine = file.readLine();
-            }
+    std::istringstream ss(m_bFilePathIsInMemoryBeatmap ? m_sFilePath.c_str() : "");
 
-            const char *curLineChar = curLine.c_str();
-            const int commentIndex = curLine.find("//");
-            if(commentIndex == std::string::npos ||
-               commentIndex != 0)  // ignore comments, but only if at the beginning of a line (e.g. allow
-                                   // Artist:DJ'TEKINA//SOMETHING)
+    // load metadata only
+    int curBlock = -1;
+    unsigned long long timingPointSortHack = 0;
+    char stringBuffer[1024];
+    std::string curLine;
+    while(!m_bFilePathIsInMemoryBeatmap ? file.canRead() : static_cast<bool>(std::getline(ss, curLine))) {
+        if(!m_bFilePathIsInMemoryBeatmap) {
+            curLine = file.readLine();
+        }
+
+        // ignore comments, but only if at the beginning of
+        // a line (e.g. allow Artist:DJ'TEKINA//SOMETHING)
+        if(curLine.find("//") == 0) continue;
+
+        const char *curLineChar = curLine.c_str();
+        if(curLine.find("[General]") != std::string::npos)
+            curBlock = 0;
+        else if(curLine.find("[Metadata]") != std::string::npos)
+            curBlock = 1;
+        else if(curLine.find("[Difficulty]") != std::string::npos)
+            curBlock = 2;
+        else if(curLine.find("[Events]") != std::string::npos)
+            curBlock = 3;
+        else if(curLine.find("[TimingPoints]") != std::string::npos)
+            curBlock = 4;
+        else if(curLine.find("[HitObjects]") != std::string::npos)
+            break;  // NOTE: stop early
+
+        switch(curBlock) {
+            case -1:  // header (e.g. "osu file format v12")
             {
-                if(curLine.find("[General]") != std::string::npos)
-                    curBlock = 0;
-                else if(curLine.find("[Metadata]") != std::string::npos)
-                    curBlock = 1;
-                else if(curLine.find("[Difficulty]") != std::string::npos)
-                    curBlock = 2;
-                else if(curLine.find("[Events]") != std::string::npos)
-                    curBlock = 3;
-                else if(curLine.find("[TimingPoints]") != std::string::npos)
-                    curBlock = 4;
-                else if(curLine.find("[HitObjects]") != std::string::npos)
-                    break;  // NOTE: stop early
+                if(sscanf(curLineChar, " osu file format v %i \n", &m_iVersion) == 1) {
+                    if(m_iVersion > osu_beatmap_version.getInt()) {
+                        debugLog("Ignoring unknown/invalid beatmap version %i\n", m_iVersion);
+                        return false;
+                    }
+                }
+            } break;
 
-                switch(curBlock) {
-                    case -1:  // header (e.g. "osu file format v12")
-                    {
-                        if(sscanf(curLineChar, " osu file format v %i \n", &databaseBeatmap->m_iVersion) == 1) {
-                            if(databaseBeatmap->m_iVersion > osu_beatmap_version.getInt()) {
-                                debugLog("Ignoring unknown/invalid beatmap version %i\n", databaseBeatmap->m_iVersion);
-                                return false;
-                            }
-                        }
-                    } break;
+            case 0:  // General
+            {
+                memset(stringBuffer, '\0', 1024);
+                if(sscanf(curLineChar, " AudioFilename : %1023[^\n]", stringBuffer) == 1) {
+                    m_sAudioFileName = stringBuffer;
+                    trim(&m_sAudioFileName);
+                }
 
-                    case 0:  // General
-                    {
-                        memset(stringBuffer, '\0', 1024);
-                        if(sscanf(curLineChar, " AudioFilename : %1023[^\n]", stringBuffer) == 1) {
-                            databaseBeatmap->m_sAudioFileName = stringBuffer;
-                            trim(&databaseBeatmap->m_sAudioFileName);
-                        }
+                sscanf(curLineChar, " StackLeniency : %f \n", &m_fStackLeniency);
+                sscanf(curLineChar, " PreviewTime : %i \n", &m_iPreviewTime);
+                sscanf(curLineChar, " Mode : %i \n", &m_iGameMode);
+            } break;
 
-                        sscanf(curLineChar, " StackLeniency : %f \n", &databaseBeatmap->m_fStackLeniency);
-                        sscanf(curLineChar, " PreviewTime : %i \n", &databaseBeatmap->m_iPreviewTime);
-                        sscanf(curLineChar, " Mode : %i \n", &databaseBeatmap->m_iGameMode);
-                    } break;
+            case 1:  // Metadata
+            {
+                memset(stringBuffer, '\0', 1024);
+                if(sscanf(curLineChar, " Title :%1023[^\n]", stringBuffer) == 1) {
+                    m_sTitle = UString(stringBuffer);
+                    trim(&m_sTitle);
+                }
 
-                    case 1:  // Metadata
-                    {
-                        memset(stringBuffer, '\0', 1024);
-                        if(sscanf(curLineChar, " Title :%1023[^\n]", stringBuffer) == 1) {
-                            databaseBeatmap->m_sTitle = UString(stringBuffer);
-                            trim(&databaseBeatmap->m_sTitle);
-                        }
+                memset(stringBuffer, '\0', 1024);
+                if(sscanf(curLineChar, " Artist :%1023[^\n]", stringBuffer) == 1) {
+                    m_sArtist = UString(stringBuffer);
+                    trim(&m_sArtist);
+                }
 
-                        memset(stringBuffer, '\0', 1024);
-                        if(sscanf(curLineChar, " Artist :%1023[^\n]", stringBuffer) == 1) {
-                            databaseBeatmap->m_sArtist = UString(stringBuffer);
-                            trim(&databaseBeatmap->m_sArtist);
-                        }
+                memset(stringBuffer, '\0', 1024);
+                if(sscanf(curLineChar, " Creator :%1023[^\n]", stringBuffer) == 1) {
+                    m_sCreator = UString(stringBuffer);
+                    trim(&m_sCreator);
+                }
 
-                        memset(stringBuffer, '\0', 1024);
-                        if(sscanf(curLineChar, " Creator :%1023[^\n]", stringBuffer) == 1) {
-                            databaseBeatmap->m_sCreator = UString(stringBuffer);
-                            trim(&databaseBeatmap->m_sCreator);
-                        }
+                memset(stringBuffer, '\0', 1024);
+                if(sscanf(curLineChar, " Version :%1023[^\n]", stringBuffer) == 1) {
+                    m_sDifficultyName = UString(stringBuffer);
+                    trim(&m_sDifficultyName);
+                }
 
-                        memset(stringBuffer, '\0', 1024);
-                        if(sscanf(curLineChar, " Version :%1023[^\n]", stringBuffer) == 1) {
-                            databaseBeatmap->m_sDifficultyName = UString(stringBuffer);
-                            trim(&databaseBeatmap->m_sDifficultyName);
-                        }
+                memset(stringBuffer, '\0', 1024);
+                if(sscanf(curLineChar, " Source :%1023[^\n]", stringBuffer) == 1) {
+                    m_sSource = stringBuffer;
+                    trim(&m_sSource);
+                }
 
-                        memset(stringBuffer, '\0', 1024);
-                        if(sscanf(curLineChar, " Source :%1023[^\n]", stringBuffer) == 1) {
-                            databaseBeatmap->m_sSource = stringBuffer;
-                            trim(&databaseBeatmap->m_sSource);
-                        }
+                memset(stringBuffer, '\0', 1024);
+                if(sscanf(curLineChar, " Tags :%1023[^\n]", stringBuffer) == 1) {
+                    m_sTags = stringBuffer;
+                    trim(&m_sTags);
+                }
 
-                        memset(stringBuffer, '\0', 1024);
-                        if(sscanf(curLineChar, " Tags :%1023[^\n]", stringBuffer) == 1) {
-                            databaseBeatmap->m_sTags = stringBuffer;
-                            trim(&databaseBeatmap->m_sTags);
-                        }
+                sscanf(curLineChar, " BeatmapID : %ld \n", &m_iID);
+                sscanf(curLineChar, " BeatmapSetID : %i \n", &m_iSetID);
+            } break;
 
-                        sscanf(curLineChar, " BeatmapID : %ld \n", &databaseBeatmap->m_iID);
-                        sscanf(curLineChar, " BeatmapSetID : %i \n", &databaseBeatmap->m_iSetID);
-                    } break;
+            case 2:  // Difficulty
+            {
+                sscanf(curLineChar, " CircleSize : %f \n", &m_fCS);
+                if(sscanf(curLineChar, " ApproachRate : %f \n", &m_fAR) == 1) foundAR = true;
+                sscanf(curLineChar, " HPDrainRate : %f \n", &m_fHP);
+                sscanf(curLineChar, " OverallDifficulty : %f \n", &m_fOD);
+                sscanf(curLineChar, " SliderMultiplier : %f \n", &m_fSliderMultiplier);
+                sscanf(curLineChar, " SliderTickRate : %f \n", &m_fSliderTickRate);
+            } break;
+
+            case 3:  // Events
+            {
+                memset(stringBuffer, '\0', 1024);
+                int type, startTime;
+                if(sscanf(curLineChar, " %i , %i , \"%1023[^\"]\"", &type, &startTime, stringBuffer) == 3) {
+                    if(type == 0) {
+                        m_sBackgroundImageFileName = UString(stringBuffer);
+                        m_sFullBackgroundImageFilePath = m_sFolder;
+                        m_sFullBackgroundImageFilePath.append(m_sBackgroundImageFileName);
+                    }
+                }
+            } break;
 
-                    case 2:  // Difficulty
+            case 4:  // TimingPoints
+            {
+                // old beatmaps: Offset, Milliseconds per Beat
+                // old new beatmaps: Offset, Milliseconds per Beat, Meter, Sample Type, Sample Set, Volume,
+                // !Inherited new new beatmaps: Offset, Milliseconds per Beat, Meter, Sample Type, Sample Set,
+                // Volume, !Inherited, Kiai Mode
+
+                double tpOffset;
+                float tpMSPerBeat;
+                int tpMeter;
+                int tpSampleType, tpSampleSet;
+                int tpVolume;
+                int tpTimingChange;
+                int tpKiai = 0;  // optional
+                if(sscanf(curLineChar, " %lf , %f , %i , %i , %i , %i , %i , %i", &tpOffset, &tpMSPerBeat, &tpMeter,
+                          &tpSampleType, &tpSampleSet, &tpVolume, &tpTimingChange, &tpKiai) == 8 ||
+                   sscanf(curLineChar, " %lf , %f , %i , %i , %i , %i , %i", &tpOffset, &tpMSPerBeat, &tpMeter,
+                          &tpSampleType, &tpSampleSet, &tpVolume, &tpTimingChange) == 7) {
+                    TIMINGPOINT t;
                     {
-                        sscanf(curLineChar, " CircleSize : %f \n", &databaseBeatmap->m_fCS);
-                        if(sscanf(curLineChar, " ApproachRate : %f \n", &databaseBeatmap->m_fAR) == 1) foundAR = true;
+                        t.offset = (long)std::round(tpOffset);
+                        t.msPerBeat = tpMSPerBeat;
 
-                        sscanf(curLineChar, " HPDrainRate : %f \n", &databaseBeatmap->m_fHP);
-                        sscanf(curLineChar, " OverallDifficulty : %f \n", &databaseBeatmap->m_fOD);
-                        sscanf(curLineChar, " SliderMultiplier : %f \n", &databaseBeatmap->m_fSliderMultiplier);
-                        sscanf(curLineChar, " SliderTickRate : %f \n", &databaseBeatmap->m_fSliderTickRate);
-                    } break;
+                        t.sampleType = tpSampleType;
+                        t.sampleSet = tpSampleSet;
+                        t.volume = tpVolume;
 
-                    case 3:  // Events
-                    {
-                        memset(stringBuffer, '\0', 1024);
-                        int type, startTime;
-                        if(sscanf(curLineChar, " %i , %i , \"%1023[^\"]\"", &type, &startTime, stringBuffer) == 3) {
-                            if(type == 0) {
-                                databaseBeatmap->m_sBackgroundImageFileName = UString(stringBuffer);
-                                databaseBeatmap->m_sFullBackgroundImageFilePath = databaseBeatmap->m_sFolder;
-                                databaseBeatmap->m_sFullBackgroundImageFilePath.append(
-                                    databaseBeatmap->m_sBackgroundImageFileName);
-                            }
-                        }
-                    } break;
+                        t.timingChange = tpTimingChange == 1;
+                        t.kiai = tpKiai > 0;
 
-                    case 4:  // TimingPoints
+                        t.sortHack = timingPointSortHack++;
+                    }
+                    m_timingpoints.push_back(t);
+                } else if(sscanf(curLineChar, " %lf , %f", &tpOffset, &tpMSPerBeat) == 2) {
+                    TIMINGPOINT t;
                     {
-                        // old beatmaps: Offset, Milliseconds per Beat
-                        // old new beatmaps: Offset, Milliseconds per Beat, Meter, Sample Type, Sample Set, Volume,
-                        // !Inherited new new beatmaps: Offset, Milliseconds per Beat, Meter, Sample Type, Sample Set,
-                        // Volume, !Inherited, Kiai Mode
+                        t.offset = (long)std::round(tpOffset);
+                        t.msPerBeat = tpMSPerBeat;
 
-                        double tpOffset;
-                        float tpMSPerBeat;
-                        int tpMeter;
-                        int tpSampleType, tpSampleSet;
-                        int tpVolume;
-                        int tpTimingChange;
-                        int tpKiai = 0;  // optional
-                        if(sscanf(curLineChar, " %lf , %f , %i , %i , %i , %i , %i , %i", &tpOffset, &tpMSPerBeat,
-                                  &tpMeter, &tpSampleType, &tpSampleSet, &tpVolume, &tpTimingChange, &tpKiai) == 8 ||
-                           sscanf(curLineChar, " %lf , %f , %i , %i , %i , %i , %i", &tpOffset, &tpMSPerBeat, &tpMeter,
-                                  &tpSampleType, &tpSampleSet, &tpVolume, &tpTimingChange) == 7) {
-                            TIMINGPOINT t;
-                            {
-                                t.offset = (long)std::round(tpOffset);
-                                t.msPerBeat = tpMSPerBeat;
-
-                                t.sampleType = tpSampleType;
-                                t.sampleSet = tpSampleSet;
-                                t.volume = tpVolume;
-
-                                t.timingChange = tpTimingChange == 1;
-                                t.kiai = tpKiai > 0;
+                        t.sampleType = 0;
+                        t.sampleSet = 0;
+                        t.volume = 100;
 
-                                t.sortHack = timingPointSortHack++;
-                            }
-                            databaseBeatmap->m_timingpoints.push_back(t);
-                        } else if(sscanf(curLineChar, " %lf , %f", &tpOffset, &tpMSPerBeat) == 2) {
-                            TIMINGPOINT t;
-                            {
-                                t.offset = (long)std::round(tpOffset);
-                                t.msPerBeat = tpMSPerBeat;
+                        t.timingChange = true;
+                        t.kiai = false;
 
-                                t.sampleType = 0;
-                                t.sampleSet = 0;
-                                t.volume = 100;
-
-                                t.timingChange = true;
-                                t.kiai = false;
-
-                                t.sortHack = timingPointSortHack++;
-                            }
-                            databaseBeatmap->m_timingpoints.push_back(t);
-                        }
-                    } break;
+                        t.sortHack = timingPointSortHack++;
+                    }
+                    m_timingpoints.push_back(t);
                 }
-            }
+            } break;
         }
     }
 
     // gamemode filter
-    if(databaseBeatmap->m_iGameMode != 0) return false;  // nothing more to do here
+    if(m_iGameMode != 0) return false;  // nothing more to do here
 
     // general sanity checks
-    if((databaseBeatmap->m_timingpoints.size() < 1)) {
+    if((m_timingpoints.size() < 1)) {
         if(Osu::debug->getBool()) debugLog("DatabaseBeatmap::loadMetadata() : no timingpoints in beatmap!\n");
-
         return false;  // nothing more to do here
     }
 
     // build sound file path
-    databaseBeatmap->m_sFullSoundFilePath = databaseBeatmap->m_sFolder;
-    databaseBeatmap->m_sFullSoundFilePath.append(databaseBeatmap->m_sAudioFileName);
+    m_sFullSoundFilePath = m_sFolder;
+    m_sFullSoundFilePath.append(m_sAudioFileName);
 
     // sort timingpoints and calculate BPM range
-    if(databaseBeatmap->m_timingpoints.size() > 0) {
-        if(Osu::debug->getBool()) debugLog("DatabaseBeatmap::loadMetadata() : calculating BPM range ...\n");
-
+    if(m_timingpoints.size() > 0) {
         // sort timingpoints by time
-        std::sort(databaseBeatmap->m_timingpoints.begin(), databaseBeatmap->m_timingpoints.end(),
-                  TimingPointSortComparator());
+        std::sort(m_timingpoints.begin(), m_timingpoints.end(), TimingPointSortComparator());
 
         // 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());
+        const auto result = db->m_starsCache.find(getMD5Hash());
         if(result != db->m_starsCache.end()) {
             if(result->second.starsNomod >= 0.f) {
-                databaseBeatmap->m_fStarsNomod = result->second.starsNomod;
+                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;
+                m_iMinBPM = result->second.min_bpm;
+                m_iMaxBPM = result->second.max_bpm;
+                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;
+            if(Osu::debug->getBool()) debugLog("DatabaseBeatmap::loadMetadata() : calculating BPM range ...\n");
+            auto bpm = getBPM(m_timingpoints);
+            m_iMinBPM = bpm.min;
+            m_iMaxBPM = bpm.max;
+            m_iMostCommonBPM = bpm.most_common;
         }
     }
 
     // special case: old beatmaps have AR = OD, there is no ApproachRate stored
-    if(!foundAR) databaseBeatmap->m_fAR = databaseBeatmap->m_fOD;
+    if(!foundAR) m_fAR = m_fOD;
 
     return true;
 }
@@ -1221,7 +1245,7 @@ DatabaseBeatmap::LOAD_GAMEPLAY_RESULT DatabaseBeatmap::loadGameplay(DatabaseBeat
 
     // NOTE: reload metadata (force ensures that all necessary data is ready for creating hitobjects and playing etc.,
     // also if beatmap file is changed manually in the meantime)
-    if(!loadMetadata(databaseBeatmap)) {
+    if(!databaseBeatmap->loadMetadata()) {
         result.errorCode = 1;
         return result;
     }
@@ -1486,45 +1510,6 @@ DatabaseBeatmap::LOAD_GAMEPLAY_RESULT DatabaseBeatmap::loadGameplay(DatabaseBeat
     return result;
 }
 
-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;
-
-    // also precalculate some largest representative values
-    m_iLengthMS = 0;
-    m_fCS = 0.0f;
-    m_fAR = 0.0f;
-    m_fOD = 0.0f;
-    m_fHP = 0.0f;
-    m_fStarsNomod = 0.0f;
-    m_iMinBPM = std::numeric_limits<int>::max();
-    m_iMaxBPM = 0;
-    m_iMostCommonBPM = 0;
-    last_modification_time = 0;
-    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();
-        if(diff->getHP() > m_fHP) m_fHP = diff->getHP();
-        if(diff->getOD() > m_fOD) m_fOD = diff->getOD();
-        if(diff->getStarsNomod() > m_fStarsNomod) m_fStarsNomod = diff->getStarsNomod();
-        if(diff->getMinBPM() < m_iMinBPM) m_iMinBPM = diff->getMinBPM();
-        if(diff->getMaxBPM() > m_iMaxBPM) m_iMaxBPM = diff->getMaxBPM();
-        if(diff->getMostCommonBPM() > m_iMostCommonBPM) m_iMostCommonBPM = diff->getMostCommonBPM();
-        if(diff->last_modification_time > last_modification_time) last_modification_time = diff->last_modification_time;
-    }
-}
-
 DatabaseBeatmap::TIMING_INFO DatabaseBeatmap::getTimingInfoForTime(unsigned long positionMS) {
     return getTimingInfoForTimeAndTimingPoints(positionMS, m_timingpoints);
 }

+ 21 - 17
src/App/Osu/DatabaseBeatmap.h

@@ -17,14 +17,17 @@ class BackgroundImageHandler;
 // 3) allow async calculations/loaders to work on the contained data (e.g. background image loader)
 // 4) be a container for difficulties (all top level DatabaseBeatmap objects are containers)
 
+class DatabaseBeatmap;
+typedef DatabaseBeatmap BeatmapDifficulty;
+typedef DatabaseBeatmap BeatmapSet;
+
 class DatabaseBeatmap {
    public:
     // raw structs
 
     struct TIMINGPOINT {
-        long offset;
-
-        float msPerBeat;
+        double offset;
+        double msPerBeat;
 
         int sampleType;
         int sampleSet;
@@ -95,11 +98,9 @@ class DatabaseBeatmap {
     static LOAD_DIFFOBJ_RESULT loadDifficultyHitObjects(const std::string &osuFilePath, float AR, float CS,
                                                         float speedMultiplier, bool calculateStarsInaccurately,
                                                         const std::atomic<bool> &dead);
-    static bool loadMetadata(DatabaseBeatmap *databaseBeatmap);
+    bool loadMetadata();
     static LOAD_GAMEPLAY_RESULT loadGameplay(DatabaseBeatmap *databaseBeatmap, Beatmap *beatmap);
 
-    void setDifficulties(std::vector<DatabaseBeatmap *> *difficulties);
-
     void setLengthMS(unsigned long lengthMS) { m_iLengthMS = lengthMS; }
 
     void setStarsNoMod(float starsNoMod) { m_fStarsNomod = starsNoMod; }
@@ -118,7 +119,10 @@ 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 {
+        static std::vector<DatabaseBeatmap *> empty;
+        return m_difficulties == nullptr ? empty : *m_difficulties;
+    }
 
     inline const MD5Hash &getMD5Hash() const { return m_sMD5Hash; }
 
@@ -451,26 +455,26 @@ struct BPMInfo getBPM(const zarray<T> &timing_points) {
 
     struct Tuple {
         i32 bpm;
-        i32 duration;
+        double duration;
     };
 
     zarray<Tuple> bpms;
     bpms.reserve(timing_points.size());
 
-    long lastTime = timing_points[timing_points.size() - 1].offset;
+    double 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) continue;
-        if(t.msPerBeat < 0) continue;
+        if(t.msPerBeat <= 0.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);
+        double currentTime = (i == 0 ? 0 : t.offset);
+        double nextTime = (i == timing_points.size() - 1 ? lastTime : timing_points[i + 1].offset);
 
-        i32 bpm = t.msPerBeat / 60000;
-        i32 duration = std::max(nextTime - currentTime, (long)0);
+        i32 bpm = std::min(60000.0 / t.msPerBeat, 9001.0);
+        double duration = std::max(nextTime - currentTime, 0.0);
 
         bool found = false;
         for(auto tuple : bpms) {
@@ -492,16 +496,16 @@ struct BPMInfo getBPM(const zarray<T> &timing_points) {
     i32 min = 9001;
     i32 max = 0;
     i32 mostCommonBPM = 0;
-    i32 longestDuration = 0;
+    double longestDuration = 0;
     for(auto tuple : bpms) {
         if(tuple.bpm > max) max = tuple.bpm;
         if(tuple.bpm < min) min = tuple.bpm;
-
-        if(tuple.duration > longestDuration) {
+        if(tuple.duration > longestDuration || (tuple.duration == longestDuration && tuple.bpm > mostCommonBPM)) {
             longestDuration = tuple.duration;
             mostCommonBPM = tuple.bpm;
         }
     }
+    if(min > max) min = max;
 
     return BPMInfo{
         .min = min,

+ 10 - 6
src/App/Osu/MainMenu.cpp

@@ -273,7 +273,7 @@ MainMenu::MainMenu(Osu *osu) : OsuScreen(osu) {
 }
 
 MainMenu::~MainMenu() {
-    // SAFE_DELETE(preloaded_beatmapset); // double free??
+    SAFE_DELETE(preloaded_beatmapset);
     SAFE_DELETE(m_updateAvailableButton);
 
     anim->deleteExistingAnimation(&m_fUpdateButtonAnim);
@@ -1083,22 +1083,26 @@ void MainMenu::selectRandomBeatmap() {
             mapset_folder.append(mapset_folders[rand() % nb_mapsets]);
             mapset_folder.append("/");
 
-            auto beatmap = m_osu->getSongBrowser()->getDatabase()->loadRawBeatmap(mapset_folder);
-            if(beatmap == nullptr) {
+            BeatmapSet *set = m_osu->getSongBrowser()->getDatabase()->loadRawBeatmap(mapset_folder);
+            if(set == nullptr) {
                 debugLog("Failed to load beatmap set '%s'\n", mapset_folder.c_str());
                 continue;
             }
 
-            auto beatmap_diffs = beatmap->getDifficulties();
+            auto beatmap_diffs = set->getDifficulties();
             if(beatmap_diffs.size() == 0) {
                 debugLog("Beatmap '%s' has no difficulties!\n", mapset_folder.c_str());
-                delete beatmap;
+                delete set;
                 continue;
             }
 
-            preloaded_beatmapset = beatmap;
+            preloaded_beatmapset = set;
+
+            // We're picking a random diff and not the first one, because diffs of the same set
+            // can have their own separate sound file.
             preloaded_beatmap = beatmap_diffs[rand() % beatmap_diffs.size()];
             preloaded_beatmap->do_not_store = true;
+
             m_osu->getSongBrowser()->onDifficultySelected(preloaded_beatmap, false);
 
             return;

+ 4 - 2
src/App/Osu/MainMenu.h

@@ -17,6 +17,8 @@ class Osu;
 
 class Beatmap;
 class DatabaseBeatmap;
+typedef DatabaseBeatmap BeatmapDifficulty;
+typedef DatabaseBeatmap BeatmapSet;
 
 class HitObject;
 
@@ -58,8 +60,8 @@ class MainMenu : public OsuScreen, public MouseListener {
     virtual void draw(Graphics *g);
     virtual void mouse_update(bool *propagate_clicks);
 
-    DatabaseBeatmap *preloaded_beatmap = nullptr;
-    DatabaseBeatmap *preloaded_beatmapset = nullptr;
+    BeatmapDifficulty *preloaded_beatmap = nullptr;
+    BeatmapSet *preloaded_beatmapset = nullptr;
     void selectRandomBeatmap();
 
     virtual void onKeyDown(KeyboardEvent &e);

+ 11 - 4
src/App/Osu/OptionsMenu.cpp

@@ -1388,10 +1388,17 @@ OptionsMenu::OptionsMenu(Osu *osu) : ScreenBackable(osu) {
 }
 
 OptionsMenu::~OptionsMenu() {
-    // TODO @kiwec: remove them from containers first
-    // SAFE_DELETE(m_asioBufferSizeSlider);
-    // SAFE_DELETE(m_wasapiBufferSizeSlider);
-    // SAFE_DELETE(m_wasapiPeriodSizeSlider);
+    m_options->getContainer()->empty();
+
+    SAFE_DELETE(m_spacer);
+    SAFE_DELETE(m_contextMenu);
+
+    for(auto element : m_elements) {
+        SAFE_DELETE(element.resetButton);
+        for(auto sub : element.elements) {
+            SAFE_DELETE(sub);
+        }
+    }
 }
 
 void OptionsMenu::draw(Graphics *g) {

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

@@ -81,7 +81,7 @@ void ScoreboardSlot::draw(Graphics *g) {
     m_avatar->setPos(0, start_y);
     m_avatar->setSize(avatar_width, avatar_height);
     m_avatar->setVisible(true);
-    m_avatar->draw(g, 0.8f * m_fAlpha);
+    m_avatar->draw_avatar(g, 0.8f * m_fAlpha);
 
     // Draw index
     g->pushTransform();

+ 14 - 56
src/App/Osu/SongBrowser.cpp

@@ -266,17 +266,7 @@ bool SongBrowser::SortByBPM::operator()(UISongBrowserButton const *a, UISongBrow
     if(a->getDatabaseBeatmap() == NULL || b->getDatabaseBeatmap() == NULL) return a->getSortHack() < b->getSortHack();
 
     int bpm1 = a->getDatabaseBeatmap()->getMostCommonBPM();
-    const std::vector<DatabaseBeatmap *> &aDiffs = a->getDatabaseBeatmap()->getDifficulties();
-    for(size_t i = 0; i < aDiffs.size(); i++) {
-        if(aDiffs[i]->getMostCommonBPM() > bpm1) bpm1 = aDiffs[i]->getMostCommonBPM();
-    }
-
     int bpm2 = b->getDatabaseBeatmap()->getMostCommonBPM();
-    const std::vector<DatabaseBeatmap *> &bDiffs = b->getDatabaseBeatmap()->getDifficulties();
-    for(size_t i = 0; i < bDiffs.size(); i++) {
-        if(bDiffs[i]->getMostCommonBPM() > bpm2) bpm2 = bDiffs[i]->getMostCommonBPM();
-    }
-
     if(bpm1 == bpm2) return a->getSortHack() < b->getSortHack();
     return bpm1 < bpm2;
 }
@@ -301,65 +291,27 @@ bool SongBrowser::SortByDateAdded::operator()(UISongBrowserButton const *a, UISo
 bool SongBrowser::SortByDifficulty::operator()(UISongBrowserButton const *a, UISongBrowserButton const *b) const {
     if(a->getDatabaseBeatmap() == NULL || b->getDatabaseBeatmap() == NULL) return a->getSortHack() < b->getSortHack();
 
+    float stars1 = a->getDatabaseBeatmap()->getStarsNomod();
+    float stars2 = b->getDatabaseBeatmap()->getStarsNomod();
+    if(stars1 != stars2) return stars1 < stars2;
+
     float diff1 = (a->getDatabaseBeatmap()->getAR() + 1) * (a->getDatabaseBeatmap()->getCS() + 1) *
                   (a->getDatabaseBeatmap()->getHP() + 1) * (a->getDatabaseBeatmap()->getOD() + 1) *
                   (std::max(a->getDatabaseBeatmap()->getMostCommonBPM(), 1));
-    float stars1 = a->getDatabaseBeatmap()->getStarsNomod();
-    const std::vector<DatabaseBeatmap *> &aDiffs = a->getDatabaseBeatmap()->getDifficulties();
-    for(size_t i = 0; i < aDiffs.size(); i++) {
-        const DatabaseBeatmap *d = aDiffs[i];
-        if(d->getStarsNomod() > stars1) stars1 = d->getStarsNomod();
-
-        const float tempDiff1 = (d->getAR() + 1) * (d->getCS() + 1) * (d->getHP() + 1) * (d->getOD() + 1) *
-                                (std::max(d->getMostCommonBPM(), 1));
-        if(tempDiff1 > diff1) diff1 = tempDiff1;
-    }
-
     float diff2 = (b->getDatabaseBeatmap()->getAR() + 1) * (b->getDatabaseBeatmap()->getCS() + 1) *
                   (b->getDatabaseBeatmap()->getHP() + 1) * (b->getDatabaseBeatmap()->getOD() + 1) *
                   (std::max(b->getDatabaseBeatmap()->getMostCommonBPM(), 1));
-    float stars2 = b->getDatabaseBeatmap()->getStarsNomod();
-    const std::vector<DatabaseBeatmap *> &bDiffs = b->getDatabaseBeatmap()->getDifficulties();
-    for(size_t i = 0; i < bDiffs.size(); i++) {
-        const DatabaseBeatmap *d = bDiffs[i];
-        if(d->getStarsNomod() > stars2) stars2 = d->getStarsNomod();
-
-        const float tempDiff2 = (d->getAR() + 1) * (d->getCS() + 1) * (d->getHP() + 1) * (d->getOD() + 1) *
-                                (std::max(d->getMostCommonBPM(), 1));
-        if(tempDiff2 > diff1) diff2 = tempDiff2;
-    }
-
-    if(stars1 > 0 && stars2 > 0) {
-        // strict weak ordering!
-        if(stars1 == stars2) return a->getSortHack() < b->getSortHack();
-
-        return stars1 < stars2;
-    } else {
-        // strict weak ordering!
-        if(diff1 == diff2) return a->getSortHack() < b->getSortHack();
+    if(diff1 != diff2) return diff1 < diff2;
 
-        return diff1 < diff2;
-    }
+    return a->getSortHack() < b->getSortHack();
 }
 
 bool SongBrowser::SortByLength::operator()(UISongBrowserButton const *a, UISongBrowserButton const *b) const {
     if(a->getDatabaseBeatmap() == NULL || b->getDatabaseBeatmap() == NULL) return a->getSortHack() < b->getSortHack();
 
     unsigned long length1 = a->getDatabaseBeatmap()->getLengthMS();
-    const std::vector<DatabaseBeatmap *> &aDiffs = a->getDatabaseBeatmap()->getDifficulties();
-    for(size_t i = 0; i < aDiffs.size(); i++) {
-        if(aDiffs[i]->getLengthMS() > length1) length1 = aDiffs[i]->getLengthMS();
-    }
-
     unsigned long length2 = b->getDatabaseBeatmap()->getLengthMS();
-    const std::vector<DatabaseBeatmap *> &bDiffs = b->getDatabaseBeatmap()->getDifficulties();
-    for(size_t i = 0; i < bDiffs.size(); i++) {
-        if(bDiffs[i]->getLengthMS() > length2) length2 = bDiffs[i]->getLengthMS();
-    }
-
-    // strict weak ordering!
     if(length1 == length2) return a->getSortHack() < b->getSortHack();
-
     return length1 < length2;
 }
 
@@ -1720,6 +1672,7 @@ void SongBrowser::refreshBeatmaps() {
 
     m_selectedBeatmap->pausePreviewMusic();
     m_selectedBeatmap->deselect();
+    SAFE_DELETE(m_selectedBeatmap);
     m_selectedBeatmap = new Beatmap(m_osu);
 
     m_selectionPreviousSongButton = NULL;
@@ -3168,14 +3121,19 @@ void SongBrowser::onDatabaseLoadingFinished() {
 
     // main menu starts playing a song before the database is loaded,
     // re-select it after the database has been loaded
-    if(m_osu->m_mainMenu->preloaded_beatmap != nullptr) {
+    if(m_osu->m_mainMenu->preloaded_beatmapset != nullptr) {
         auto matching_beatmap = getDatabase()->getBeatmapDifficulty(m_osu->m_mainMenu->preloaded_beatmap->getMD5Hash());
         if(matching_beatmap) {
             onDifficultySelected(matching_beatmap, false);
             selectSelectedBeatmapSongButton();
         }
 
-        SAFE_DELETE(m_osu->m_mainMenu->preloaded_beatmap);
+        SAFE_DELETE(m_osu->m_mainMenu->preloaded_beatmapset);
+    }
+
+    // ok, if we still haven't selected a song, do so now
+    if(m_selectedBeatmap->getSelectedDifficulty2() == nullptr) {
+        selectRandomBeatmap();
     }
 }
 

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

@@ -81,12 +81,10 @@ UIAvatar::UIAvatar(u32 player_id, float xPos, float yPos, float xSize, float ySi
 }
 
 UIAvatar::~UIAvatar() {
-    if(avatar != nullptr) {
-        engine->getResourceManager()->destroyResource(avatar);
-    }
+    // XXX: leaking avatar Resource here, because we don't know in how many places it will be reused
 }
 
-void UIAvatar::draw(Graphics *g, float alpha) {
+void UIAvatar::draw_avatar(Graphics *g, float alpha) {
     if(!on_screen) return;  // Comment when you need to debug on_screen logic
 
     if(avatar == nullptr) {

+ 2 - 1
src/App/Osu/UIAvatar.h

@@ -8,7 +8,8 @@ class UIAvatar : public CBaseUIButton {
     UIAvatar(u32 player_id, float xPos, float yPos, float xSize, float ySize);
     ~UIAvatar();
 
-    virtual void draw(Graphics *g, float alpha = 1.f);
+    virtual void draw(Graphics *g) { draw_avatar(g, 1.f); }
+    void draw_avatar(Graphics *g, float alpha);
 
     void onAvatarClicked(CBaseUIButton *btn);
 

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

@@ -3,9 +3,9 @@
 #include "Database.h"
 
 class Osu;
-class FinishedScore;
 class LiveScore;
 class SkinImage;
+struct FinishedScore;
 
 class UIRankingScreenRankingPanel : public CBaseUIImage {
    public:

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

@@ -106,7 +106,7 @@ void UISongBrowserScoreButton::draw(Graphics *g) {
         bool is_below_top = m_avatar->getPos().y + m_avatar->getSize().y >= m_scoreBrowser->getPos().y;
         bool is_above_bottom = m_avatar->getPos().y <= m_scoreBrowser->getPos().y + m_scoreBrowser->getSize().y;
         m_avatar->on_screen = is_below_top && is_above_bottom;
-        m_avatar->draw(g, 1.f);
+        m_avatar->draw_avatar(g, 1.f);
     }
     const float indexNumberScale = 0.35f;
     const float indexNumberWidthPercent = (m_style == STYLE::TOP_RANKS ? 0.075f : 0.15f);

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

@@ -67,7 +67,7 @@ void UISongBrowserUserButton::draw(Graphics *g) {
     if(m_avatar) {
         m_avatar->setPos(m_vPos.x + iconBorder + 1, m_vPos.y + iconBorder + 1);
         m_avatar->setSize(iconWidth, iconHeight);
-        m_avatar->draw(g, 1.f);
+        m_avatar->draw_avatar(g, 1.f);
     } else {
         g->setColor(0xffffffff);
         g->pushClipRect(McRect(m_vPos.x + iconBorder + 1, m_vPos.y + iconBorder + 2, iconWidth, iconHeight));

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

@@ -127,12 +127,11 @@ class UserStatsScreenBackgroundPPRecalculator : public Resource {
                         m_osu->getSongBrowser()->getDatabase()->getBeatmapDifficulty(score.md5hash);
                     if(diff2 == NULL) {
                         if(Osu::debug->getBool()) debugLog("PPRecalc couldn't find %s\n", score.md5hash.toUtf8());
-
                         continue;
                     }
 
                     // 1.5) reload metadata for sanity (maybe osu!.db has outdated AR/CS/OD/HP or some other shit)
-                    if(!DatabaseBeatmap::loadMetadata(diff2)) continue;
+                    if(!diff2->loadMetadata()) continue;
 
                     const Replay::BEATMAP_VALUES legacyValues = Replay::getBeatmapValuesForModsLegacy(
                         score.modsLegacy, diff2->getAR(), diff2->getCS(), diff2->getOD(), diff2->getHP());

+ 1 - 1
src/Util/cbase.h

@@ -166,7 +166,7 @@ struct zarray {
         nb = new_nb;
     }
 
-    void swap(zarray<T> other) {
+    void swap(zarray<T> &other) {
         size_t omax = max;
         size_t onb = nb;
         T *omemory = memory;