1
0

4 Коммиты bd1efc3266 ... 56aae671e6

Автор SHA1 Сообщение Дата
  Clément Wolf 56aae671e6 Misc fixes 2 недель назад
  Clément Wolf ccb8fed989 Don't pause song playback on first song database load 2 недель назад
  Clément Wolf 551a5591aa Allow volume overlay while song database is loading 2 недель назад
  Clément Wolf 4aa4260e5b Fix shutdown taking too long 2 недель назад

+ 11 - 11
src/App/Osu/Bancho.cpp

@@ -504,13 +504,13 @@ Packet build_login_packet() {
     Packet packet;
 
     write_bytes(&packet, (u8 *)bancho.username.toUtf8(), bancho.username.lengthUtf8());
-    write_u8(&packet, '\n');
+    write<u8>(&packet, '\n');
 
     write_bytes(&packet, (u8 *)bancho.pw_md5.hash, 32);
-    write_u8(&packet, '\n');
+    write<u8>(&packet, '\n');
 
     write_bytes(&packet, (u8 *)OSU_VERSION, strlen(OSU_VERSION));
-    write_u8(&packet, '|');
+    write<u8>(&packet, '|');
 
     // UTC offset
     time_t now = time(NULL);
@@ -518,15 +518,15 @@ Packet build_login_packet() {
     auto local_time = localtime(&now);
     int utc_offset = difftime(mktime(local_time), mktime(gmt)) / 3600;
     if(utc_offset < 0) {
-        write_u8(&packet, '-');
+        write<u8>(&packet, '-');
         utc_offset *= -1;
     }
-    write_u8(&packet, '0' + utc_offset);
-    write_u8(&packet, '|');
+    write<u8>(&packet, '0' + utc_offset);
+    write<u8>(&packet, '|');
 
     // Don't dox the user's city
-    write_u8(&packet, '0');
-    write_u8(&packet, '|');
+    write<u8>(&packet, '0');
+    write<u8>(&packet, '|');
 
     char osu_path[PATH_MAX] = {0};
 #ifdef _WIN32
@@ -556,9 +556,9 @@ Packet build_login_packet() {
     write_bytes(&packet, (u8 *)bancho.client_hashes.toUtf8(), bancho.client_hashes.lengthUtf8());
 
     // Allow PMs from strangers
-    write_u8(&packet, '|');
-    write_u8(&packet, '0');
+    write<u8>(&packet, '|');
+    write<u8>(&packet, '0');
 
-    write_u8(&packet, '\n');
+    write<u8>(&packet, '\n');
     return packet;
 }

+ 27 - 13
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);
 
@@ -51,9 +58,9 @@ void disconnect() {
     // This is a blocking call, but we *do* want this to block when quitting the game.
     if(bancho.is_online()) {
         Packet packet;
-        write_u16(&packet, LOGOUT);
-        write_u8(&packet, 0);
-        write_u32(&packet, 0);
+        write<u16>(&packet, LOGOUT);
+        write<u8>(&packet, 0);
+        write<u32>(&packet, 0);
 
         CURL *curl = curl_easy_init();
         auto version_header = UString::format("x-mcosu-ver: %s", bancho.neosu_version.toUtf8());
@@ -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");
@@ -309,7 +317,6 @@ static void *do_networking(void *data) {
 
     last_packet_tms = time(NULL);
 
-    curl_global_init(CURL_GLOBAL_ALL);
     CURL *curl = curl_easy_init();
     if(!curl) {
         debugLog("Failed to initialize cURL, online functionality disabled.\n");
@@ -343,9 +350,9 @@ static void *do_networking(void *data) {
             free(login.memory);
             pthread_mutex_lock(&outgoing_mutex);
         } else if(should_ping && outgoing.pos == 0) {
-            write_u16(&outgoing, PING);
-            write_u8(&outgoing, 0);
-            write_u32(&outgoing, 0);
+            write<u16>(&outgoing, PING);
+            write<u8>(&outgoing, 0);
+            write<u32>(&outgoing, 0);
 
             // Polling gets slower over time, but resets when we receive new data
             if(seconds_between_pings < 30.0) {
@@ -360,9 +367,9 @@ static void *do_networking(void *data) {
 
             // DEBUG: If we're not sending the right amount of bytes, bancho.py just
             // chugs along! To try to detect it faster, we'll send two packets per request.
-            write_u16(&out, PING);
-            write_u8(&out, 0);
-            write_u32(&out, 0);
+            write<u16>(&out, PING);
+            write<u8>(&out, 0);
+            write<u32>(&out, 0);
 
             send_bancho_packet(curl, out);
             free(out.memory);
@@ -482,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;
     }
 
@@ -495,12 +505,16 @@ void send_packet(Packet &packet) {
 
     // We're not sending it immediately, instead we just add it to the pile of
     // packets to send
-    write_u16(&outgoing, packet.id);
-    write_u8(&outgoing, 0);
-    write_u32(&outgoing, packet.pos);
+    write<u16>(&outgoing, packet.id);
+    write<u8>(&outgoing, 0);
+    write<u32>(&outgoing, packet.pos);
     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() {

+ 27 - 33
src/App/Osu/BanchoProtocol.cpp

@@ -65,39 +65,39 @@ Room::Room(Packet *packet) {
 }
 
 void Room::pack(Packet *packet) {
-    write_u16(packet, id);
-    write_u8(packet, in_progress);
-    write_u8(packet, match_type);
-    write_u32(packet, mods);
+    write<u16>(packet, id);
+    write<u8>(packet, in_progress);
+    write<u8>(packet, match_type);
+    write<u32>(packet, mods);
     write_string(packet, name.toUtf8());
     write_string(packet, password.toUtf8());
     write_string(packet, map_name.toUtf8());
-    write_u32(packet, map_id);
+    write<u32>(packet, map_id);
     write_string(packet, map_md5.toUtf8());
     for(int i = 0; i < 16; i++) {
-        write_u8(packet, slots[i].status);
+        write<u8>(packet, slots[i].status);
     }
     for(int i = 0; i < 16; i++) {
-        write_u8(packet, slots[i].team);
+        write<u8>(packet, slots[i].team);
     }
     for(int s = 0; s < 16; s++) {
         if(slots[s].has_player()) {
-            write_u32(packet, slots[s].player_id);
+            write<u32>(packet, slots[s].player_id);
         }
     }
 
-    write_u32(packet, host_id);
-    write_u8(packet, mode);
-    write_u8(packet, win_condition);
-    write_u8(packet, team_type);
-    write_u8(packet, freemods);
+    write<u32>(packet, host_id);
+    write<u8>(packet, mode);
+    write<u8>(packet, win_condition);
+    write<u8>(packet, team_type);
+    write<u8>(packet, freemods);
     if(freemods) {
         for(int i = 0; i < 16; i++) {
-            write_u32(packet, slots[i].mods);
+            write<u32>(packet, slots[i].mods);
         }
     }
 
-    write_u32(packet, seed);
+    write<u32>(packet, seed);
 }
 
 bool Room::is_host() { return host_id == bancho.user_id; }
@@ -182,8 +182,8 @@ void skip_string(Packet *packet) {
 
 void write_bytes(Packet *packet, u8 *bytes, size_t n) {
     if(packet->pos + n > packet->size) {
-        packet->memory = (unsigned char *)realloc(packet->memory, packet->size + n + 128);
-        packet->size += n + 128;
+        packet->memory = (unsigned char *)realloc(packet->memory, packet->size + n + 4096);
+        packet->size += n + 4096;
         if(!packet->memory) return;
     }
 
@@ -191,18 +191,10 @@ void write_bytes(Packet *packet, u8 *bytes, size_t n) {
     packet->pos += n;
 }
 
-void write_u8(Packet *packet, u8 b) { write_bytes(packet, &b, 1); }
-
-void write_u16(Packet *packet, u16 s) { write_bytes(packet, (u8 *)&s, 2); }
-
-void write_u32(Packet *packet, u32 i) { write_bytes(packet, (u8 *)&i, 4); }
-
-void write_u64(Packet *packet, u64 i) { write_bytes(packet, (u8 *)&i, 8); }
-
 void write_uleb128(Packet *packet, u32 num) {
     if(num == 0) {
         u8 zero = 0;
-        write_u8(packet, zero);
+        write<u8>(packet, zero);
         return;
     }
 
@@ -212,25 +204,27 @@ void write_uleb128(Packet *packet, u32 num) {
         if(num != 0) {
             next |= 0x80;
         }
-        write_u8(packet, next);
+        write<u8>(packet, next);
     }
 }
 
-void write_f32(Packet *packet, float f) { write_bytes(packet, (u8 *)&f, 4); }
-
-void write_f64(Packet *packet, double f) { write_bytes(packet, (u8 *)&f, 8); }
-
 void write_string(Packet *packet, const char *str) {
     if(!str || str[0] == '\0') {
         u8 zero = 0;
-        write_u8(packet, zero);
+        write<u8>(packet, zero);
         return;
     }
 
     u8 empty_check = 11;
-    write_u8(packet, empty_check);
+    write<u8>(packet, empty_check);
 
     u32 len = strlen(str);
     write_uleb128(packet, len);
     write_bytes(packet, (u8 *)str, len);
 }
+
+void write_hash(Packet *packet, MD5Hash hash) {
+    write<u8>(packet, 0x0B);
+    write<u8>(packet, 0x20);
+    write_bytes(packet, (u8 *)hash.hash, 32);
+}

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

@@ -141,6 +141,12 @@ struct Packet {
     size_t pos = 0;
     u8 *extra = nullptr;
     u32 extra_int = 0;  // lazy
+
+    void reserve(u32 newsize) {
+        if(newsize <= size) return;
+        memory = (u8 *)realloc(memory, newsize);
+        size = newsize;
+    }
 };
 
 struct Slot {
@@ -268,23 +274,11 @@ T read(Packet *packet) {
 }
 
 void write_bytes(Packet *packet, u8 *bytes, size_t n);
-void write_u8(Packet *packet, u8 b);
-void write_u16(Packet *packet, u16 s);
-void write_u32(Packet *packet, u32 i);
-void write_u64(Packet *packet, u64 i);
-void write_f32(Packet *packet, f32 f);
-void write_f64(Packet *packet, f64 f);
 void write_uleb128(Packet *packet, u32 num);
 void write_string(Packet *packet, const char *str);
+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) + 128);
-        packet->size += sizeof(T) + 128;
-        if(!packet->memory) return;
-    }
-
-    *(packet->memory + packet->pos) = t;
-    packet->pos += sizeof(T);
+    write_bytes(packet, (u8 *)&t, sizeof(T));
 }

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

@@ -1726,11 +1726,10 @@ void Beatmap::loadMusic(bool stream, bool prescan) {
 
 void Beatmap::unloadMusic() {
     if(m_osu->getInstanceID() < 2) {
-        engine->getSound()->stop(m_music);
         engine->getResourceManager()->destroyResource(m_music);
     }
 
-    m_music = NULL;
+    m_music = nullptr;
 }
 
 void Beatmap::unloadObjects() {

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

@@ -258,6 +258,8 @@ class Beatmap {
     inline const std::vector<HitObject *> &getHitObjectsPointer() const { return m_hitobjects; }
     inline float getBreakBackgroundFadeAnim() const { return m_fBreakBackgroundFade; }
 
+    Sound *m_music;
+
    protected:
     // internal
     bool canDraw();
@@ -295,7 +297,6 @@ class Beatmap {
     DatabaseBeatmap *m_selectedDifficulty2;
 
     // sound
-    Sound *m_music;
     float m_fMusicFrequencyBackup;
     long m_iCurMusicPos;
     long m_iCurMusicPosWithOffsets;

+ 11 - 8
src/App/Osu/Changelog.cpp

@@ -45,20 +45,23 @@ Changelog::Changelog(Osu *osu) : ScreenBackable(osu) {
     latest.changes.push_back("- Changed scoreboard name color to red for friends");
     latest.changes.push_back("- Changed default instant replay key to F2 to avoid conflicts with mod selector");
     latest.changes.push_back("- Disabled score submission when mods are toggled mid-game");
-    latest.changes.push_back("- Forced exclusive mode when using WASAPI output");
-    latest.changes.push_back("- Optimized beatmap list loading speed (a lot)");
-    latest.changes.push_back("- Optimized collection processing speed");
-    latest.changes.push_back("- Removed support for the Nintendo Switch");
-    latest.changes.push_back("- Updated protocol version");
+    latest.changes.push_back("- Fixed ALT key not working on linux");
     latest.changes.push_back("- Fixed chat layout updating while chat was hidden");
-    latest.changes.push_back("- Fixed pause button not working after cancelling database load");
-    latest.changes.push_back("- Fixed level bar always being at 0%");
     latest.changes.push_back("- Fixed experimental mods not getting set while watching replays");
     latest.changes.push_back("- Fixed FPoSu camera not following cursor while watching replays");
     latest.changes.push_back("- Fixed FPoSu mod not being included in score data");
+    latest.changes.push_back("- Fixed level bar always being at 0%");
+    latest.changes.push_back("- Fixed music pausing on first song database load");
+    latest.changes.push_back("- Fixed not being able to adjust volume while song database was loading");
+    latest.changes.push_back("- Fixed pause button not working after cancelling database load");
     latest.changes.push_back("- Fixed replay playback starting too fast");
     latest.changes.push_back("- Fixed restarting SoundEngine not kicking the player out of play mode");
-    latest.changes.push_back("- Fixed ALT key not working on linux");
+    latest.changes.push_back("- Improved audio engine");
+    latest.changes.push_back("- Improved overall stability");
+    latest.changes.push_back("- Optimized song database loading speed (a lot)");
+    latest.changes.push_back("- Optimized collection processing speed");
+    latest.changes.push_back("- Removed support for the Nintendo Switch");
+    latest.changes.push_back("- Updated protocol version");
     changelogs.push_back(latest);
 
     CHANGELOG v34_10;

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

@@ -245,7 +245,7 @@ void Chat::onKeyDown(KeyboardEvent &key) {
             write_string(&packet, (char *)bancho.username.toUtf8());
             write_string(&packet, (char *)m_input_box->getText().toUtf8());
             write_string(&packet, (char *)m_selected_channel->name.toUtf8());
-            write_u32(&packet, bancho.user_id);
+            write<u32>(&packet, bancho.user_id);
             send_packet(packet);
 
             // Server doesn't echo the message back

+ 4 - 4
src/App/Osu/Collections.cpp

@@ -208,22 +208,22 @@ bool save_collections() {
     const double startTime = engine->getTimeReal();
 
     Packet db;
-    write_u32(&db, COLLECTIONS_DB_VERSION);
+    write<u32>(&db, COLLECTIONS_DB_VERSION);
 
     u32 nb_collections = collections.size();
-    write_u32(&db, nb_collections);
+    write<u32>(&db, nb_collections);
 
     for(auto collection : collections) {
         write_string(&db, collection->name.c_str());
 
         u32 nb_deleted = collection->deleted_maps.size();
-        write_u32(&db, nb_deleted);
+        write<u32>(&db, nb_deleted);
         for(auto map : collection->deleted_maps) {
             write_string(&db, map.hash);
         }
 
         u32 nb_neosu = collection->neosu_maps.size();
-        write_u32(&db, nb_neosu);
+        write<u32>(&db, nb_neosu);
         for(auto map : collection->neosu_maps) {
             write_string(&db, map.hash);
         }

+ 97 - 138
src/App/Osu/Database.cpp

@@ -294,8 +294,10 @@ class DatabaseLoader : public Resource {
             m_toCleanup.swap(m_db->m_databaseBeatmaps);
             m_db->m_databaseBeatmaps.clear();
             m_db->loadDB(&db, m_bNeedRawLoad);
-        } else
+        } else {
             m_bNeedRawLoad = true;
+        }
+        free(db.memory);
 
         m_bAsyncReady = true;
     }
@@ -368,6 +370,8 @@ Database::~Database() {
     for(int i = 0; i < m_scoreSortingMethods.size(); i++) {
         delete m_scoreSortingMethods[i].comparator;
     }
+
+    unload_collections();
 }
 
 void Database::update() {
@@ -441,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;
 }
 
@@ -986,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
@@ -1266,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 *>();
@@ -1275,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(m_bInterruptLoad.load()) break;            // cancellation point
+        if(beatmapSets[i].diffs2->empty()) continue;  // sanity check
 
-        if(!beatmapSets[i].diffs2->empty())  // sanity check
-        {
-            if(beatmapSets[i].setID > 0) {
-                DatabaseBeatmap *bm = new DatabaseBeatmap(m_osu, beatmapSets[i].diffs2);
-
-                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);
             }
         }
     }
@@ -1393,6 +1356,8 @@ void Database::loadStars() {
 
 void Database::saveStars() {
     debugLog("Osu: Saving stars ...\n");
+    Timer t;
+    t.start();
 
     i64 numStarsCacheEntries = 0;
     for(DatabaseBeatmap *beatmap : m_databaseBeatmaps) {
@@ -1409,13 +1374,15 @@ void Database::saveStars() {
 
     // write
     Packet cache;
-    write_u32(&cache, STARS_CACHE_VERSION);
+    write<u32>(&cache, STARS_CACHE_VERSION);
     write_string(&cache, "00000000000000000000000000000000");
-    write_u64(&cache, numStarsCacheEntries);
-    for(DatabaseBeatmap *beatmap : m_databaseBeatmaps) {
-        for(DatabaseBeatmap *diff2 : beatmap->getDifficulties()) {
-            write_string(&cache, diff2->getMD5Hash().hash);
-            write_f32(&cache, diff2->getStarsNomod());
+    write<u64>(&cache, numStarsCacheEntries);
+
+    cache.reserve(cache.size + (34 + 4 + 4 + 4 + 4) * numStarsCacheEntries);
+    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());
             write<i32>(&cache, diff2->getMaxBPM());
             write<i32>(&cache, diff2->getMostCommonBPM());
@@ -1427,6 +1394,9 @@ void Database::saveStars() {
     }
 
     free(cache.memory);
+
+    t.update();
+    debugLog("Saving stars took %f seconds.\n", t.getElapsedTime());
 }
 
 void Database::loadScores() {
@@ -1704,8 +1674,8 @@ void Database::saveScores() {
 
     // write header
     Packet db;
-    write_u32(&db, LiveScore::VERSION);
-    write_u32(&db, numBeatmaps);
+    write<u32>(&db, LiveScore::VERSION);
+    write<u32>(&db, numBeatmaps);
 
     // write scores for each beatmap
     for(auto &beatmap : m_scores) {
@@ -1717,55 +1687,55 @@ void Database::saveScores() {
         if(numNonLegacyScores == 0) continue;
 
         write_string(&db, beatmap.first.hash);  // beatmap md5 hash
-        write_u32(&db, numNonLegacyScores);     // numScores
+        write<u32>(&db, numNonLegacyScores);    // numScores
 
         for(auto &score : beatmap.second) {
             if(score.isLegacyScore) continue;
 
             u8 gamemode = 0;
             if(score.version > 20190103 && score.isImportedLegacyScore) gamemode = hackIsImportedLegacyScoreFlag;
-            write_u8(&db, gamemode);
+            write<u8>(&db, gamemode);
 
-            write_u32(&db, score.version);
-            write_u64(&db, score.unixTimestamp);
+            write<u32>(&db, score.version);
+            write<u64>(&db, score.unixTimestamp);
 
             // default
             write_string(&db, score.playerName.c_str());
 
-            write_u16(&db, score.num300s);
-            write_u16(&db, score.num100s);
-            write_u16(&db, score.num50s);
-            write_u16(&db, score.numGekis);
-            write_u16(&db, score.numKatus);
-            write_u16(&db, score.numMisses);
+            write<u16>(&db, score.num300s);
+            write<u16>(&db, score.num100s);
+            write<u16>(&db, score.num50s);
+            write<u16>(&db, score.numGekis);
+            write<u16>(&db, score.numKatus);
+            write<u16>(&db, score.numMisses);
 
-            write_u64(&db, score.score);
-            write_u16(&db, score.comboMax);
-            write_u32(&db, score.modsLegacy);
+            write<u64>(&db, score.score);
+            write<u16>(&db, score.comboMax);
+            write<u32>(&db, score.modsLegacy);
 
             // custom
-            write_u16(&db, score.numSliderBreaks);
-            write_f32(&db, score.pp);
-            write_f32(&db, score.unstableRate);
-            write_f32(&db, score.hitErrorAvgMin);
-            write_f32(&db, score.hitErrorAvgMax);
-            write_f32(&db, score.starsTomTotal);
-            write_f32(&db, score.starsTomAim);
-            write_f32(&db, score.starsTomSpeed);
-            write_f32(&db, score.speedMultiplier);
-            write_f32(&db, score.CS);
-            write_f32(&db, score.AR);
-            write_f32(&db, score.OD);
-            write_f32(&db, score.HP);
+            write<u16>(&db, score.numSliderBreaks);
+            write<f32>(&db, score.pp);
+            write<f32>(&db, score.unstableRate);
+            write<f32>(&db, score.hitErrorAvgMin);
+            write<f32>(&db, score.hitErrorAvgMax);
+            write<f32>(&db, score.starsTomTotal);
+            write<f32>(&db, score.starsTomAim);
+            write<f32>(&db, score.starsTomSpeed);
+            write<f32>(&db, score.speedMultiplier);
+            write<f32>(&db, score.CS);
+            write<f32>(&db, score.AR);
+            write<f32>(&db, score.OD);
+            write<f32>(&db, score.HP);
 
             if(score.version > 20180722) {
-                write_u32(&db, score.maxPossibleCombo);
-                write_u32(&db, score.numHitObjects);
-                write_u32(&db, score.numCircles);
+                write<u32>(&db, score.maxPossibleCombo);
+                write<u32>(&db, score.numHitObjects);
+                write<u32>(&db, score.numCircles);
             }
 
             if(score.version >= 20240412) {
-                write_u32(&db, score.online_score_id);
+                write<u32>(&db, score.online_score_id);
                 write_string(&db, score.server.c_str());
             }
 
@@ -1780,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,

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

@@ -199,7 +199,7 @@ void Lobby::addRoom(Room* room) {
 void Lobby::joinRoom(u32 id, UString password) {
     Packet packet;
     packet.id = JOIN_ROOM;
-    write_u32(&packet, id);
+    write<u32>(&packet, id);
     write_string(&packet, password.toUtf8());
     send_packet(packet);
 

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

@@ -1054,9 +1054,7 @@ void MainMenu::mouse_update(bool *propagate_clicks) {
     } else {
         if(music->isFinished()) {
             selectRandomBeatmap();
-        }
-
-        if(music->isPlaying()) {
+        } else if(music->isPlaying()) {
             m_pauseButton->setPaused(false);
 
             // NOTE: We set this every frame, because music loading isn't instant
@@ -1085,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);

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

@@ -1188,7 +1188,7 @@ void ModSelector::resetModsUserInitiated() {
 
             Packet packet;
             packet.id = MATCH_CHANGE_MODS;
-            write_u32(&packet, bancho.room.slots[i].mods);
+            write<u32>(&packet, bancho.room.slots[i].mods);
             send_packet(packet);
 
             m_osu->m_room->updateLayout(m_osu->getScreenSize());

+ 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) {

+ 8 - 8
src/App/Osu/RichPresence.cpp

@@ -61,12 +61,12 @@ void RichPresence::setBanchoStatus(Osu *osu, const char *info_text, Action actio
 
     Packet packet;
     packet.id = CHANGE_ACTION;
-    write_u8(&packet, action);
+    write<u8>(&packet, action);
     write_string(&packet, fancy_text);
     write_string(&packet, map_md5.hash);
-    write_u32(&packet, osu->m_modSelector->getModFlags());
-    write_u8(&packet, 0);  // osu!std
-    write_u32(&packet, map_id);
+    write<u32>(&packet, osu->m_modSelector->getModFlags());
+    write<u8>(&packet, 0);  // osu!std
+    write<u32>(&packet, map_id);
     send_packet(packet);
 }
 
@@ -85,12 +85,12 @@ void RichPresence::updateBanchoMods() {
 
     Packet packet;
     packet.id = CHANGE_ACTION;
-    write_u8(&packet, last_action);
+    write<u8>(&packet, last_action);
     write_string(&packet, last_status.toUtf8());
     write_string(&packet, map_md5.hash);
-    write_u32(&packet, bancho.osu->m_modSelector->getModFlags());
-    write_u8(&packet, 0);  // osu!std
-    write_u32(&packet, map_id);
+    write<u32>(&packet, bancho.osu->m_modSelector->getModFlags());
+    write<u8>(&packet, 0);  // osu!std
+    write<u32>(&packet, map_id);
     send_packet(packet);
 
     // Servers like akatsuki send different leaderboards based on what mods

+ 17 - 17
src/App/Osu/RoomScreen.cpp

@@ -798,7 +798,7 @@ void RoomScreen::onClientScoreChange(bool force) {
     Packet packet;
     packet.id = UPDATE_MATCH_SCORE;
 
-    write_u32(&packet, (i32)m_osu->getSelectedBeatmap()->getTime());
+    write<u32>(&packet, (i32)m_osu->getSelectedBeatmap()->getTime());
 
     u8 slot_id = 0;
     for(u8 i = 0; i < 16; i++) {
@@ -807,22 +807,22 @@ void RoomScreen::onClientScoreChange(bool force) {
             break;
         }
     }
-    write_u8(&packet, slot_id);
-
-    write_u16(&packet, (u16)m_osu->getScore()->getNum300s());
-    write_u16(&packet, (u16)m_osu->getScore()->getNum100s());
-    write_u16(&packet, (u16)m_osu->getScore()->getNum50s());
-    write_u16(&packet, (u16)m_osu->getScore()->getNum300gs());
-    write_u16(&packet, (u16)m_osu->getScore()->getNum100ks());
-    write_u16(&packet, (u16)m_osu->getScore()->getNumMisses());
-    write_u32(&packet, (i32)m_osu->getScore()->getScore());
-    write_u16(&packet, (u16)m_osu->getScore()->getCombo());
-    write_u16(&packet, (u16)m_osu->getScore()->getComboMax());
-    write_u8(&packet, m_osu->getScore()->getNumSliderBreaks() == 0 && m_osu->getScore()->getNumMisses() == 0 &&
-                          m_osu->getScore()->getNum50s() == 0 && m_osu->getScore()->getNum100s() == 0);
-    write_u8(&packet, m_osu->getSelectedBeatmap()->getHealth() * 200);
-    write_u8(&packet, 0);  // 4P, not supported
-    write_u8(&packet, m_osu->getModScorev2());
+    write<u8>(&packet, slot_id);
+
+    write<u16>(&packet, (u16)m_osu->getScore()->getNum300s());
+    write<u16>(&packet, (u16)m_osu->getScore()->getNum100s());
+    write<u16>(&packet, (u16)m_osu->getScore()->getNum50s());
+    write<u16>(&packet, (u16)m_osu->getScore()->getNum300gs());
+    write<u16>(&packet, (u16)m_osu->getScore()->getNum100ks());
+    write<u16>(&packet, (u16)m_osu->getScore()->getNumMisses());
+    write<u32>(&packet, (i32)m_osu->getScore()->getScore());
+    write<u16>(&packet, (u16)m_osu->getScore()->getCombo());
+    write<u16>(&packet, (u16)m_osu->getScore()->getComboMax());
+    write<u8>(&packet, m_osu->getScore()->getNumSliderBreaks() == 0 && m_osu->getScore()->getNumMisses() == 0 &&
+                           m_osu->getScore()->getNum50s() == 0 && m_osu->getScore()->getNum100s() == 0);
+    write<u8>(&packet, m_osu->getSelectedBeatmap()->getHealth() * 200);
+    write<u8>(&packet, 0);  // 4P, not supported
+    write<u8>(&packet, m_osu->getModScorev2());
     send_packet(packet);
 
     last_packet_tms = time(NULL);

+ 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();

+ 21 - 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(diff1 != diff2) return diff1 < diff2;
 
-    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();
-
-        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;
 }
 
@@ -1711,8 +1663,16 @@ void SongBrowser::refreshBeatmaps() {
     checkHandleKillDynamicStarCalculator(false);
     checkHandleKillBackgroundSearchMatcher();
 
+    // don't pause the music the first time we load the song database
+    static bool first_refresh = true;
+    if(first_refresh) {
+        m_selectedBeatmap->m_music = nullptr;
+        first_refresh = false;
+    }
+
     m_selectedBeatmap->pausePreviewMusic();
     m_selectedBeatmap->deselect();
+    SAFE_DELETE(m_selectedBeatmap);
     m_selectedBeatmap = new Beatmap(m_osu);
 
     m_selectionPreviousSongButton = NULL;
@@ -3161,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/UIModSelectorModButton.cpp

@@ -138,7 +138,7 @@ void UIModSelectorModButton::onClicked() {
             debugLog("Sending mod change to server.\n");
             Packet packet;
             packet.id = MATCH_CHANGE_MODS;
-            write_u32(&packet, bancho.room.slots[i].mods);
+            write<u32>(&packet, bancho.room.slots[i].mods);
             send_packet(packet);
 
             m_osu->m_room->on_room_updated(bancho.room);

+ 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));

+ 4 - 4
src/App/Osu/UIUserContextMenu.cpp

@@ -96,12 +96,12 @@ void UIUserContextMenuScreen::on_action(UString text, int user_action) {
     if(user_action == UA_TRANSFER_HOST) {
         Packet packet;
         packet.id = TRANSFER_HOST;
-        write_u32(&packet, slot_number);
+        write<u32>(&packet, slot_number);
         send_packet(packet);
     } else if(user_action == KICK) {
         Packet packet;
         packet.id = MATCH_LOCK;
-        write_u32(&packet, slot_number);
+        write<u32>(&packet, slot_number);
         send_packet(packet);  // kick by locking the slot
         send_packet(packet);  // unlock the slot
     } else if(user_action == START_CHAT) {
@@ -113,13 +113,13 @@ void UIUserContextMenuScreen::on_action(UString text, int user_action) {
     } else if(user_action == UA_ADD_FRIEND) {
         Packet packet;
         packet.id = FRIEND_ADD;
-        write_u32(&packet, m_user_id);
+        write<u32>(&packet, m_user_id);
         send_packet(packet);
         friends.push_back(m_user_id);
     } else if(user_action == UA_REMOVE_FRIEND) {
         Packet packet;
         packet.id = FRIEND_REMOVE;
-        write_u32(&packet, m_user_id);
+        write<u32>(&packet, m_user_id);
         send_packet(packet);
 
         auto it = std::find(friends.begin(), friends.end(), m_user_id);

+ 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());

+ 3 - 1
src/App/Osu/VolumeOverlay.cpp

@@ -243,7 +243,9 @@ bool VolumeOverlay::isVisible() { return engine->getTime() < m_fVolumeChangeTime
 
 bool VolumeOverlay::canChangeVolume() {
     bool can_scroll = true;
-    if(m_osu->m_songBrowser2->isVisible()) can_scroll = false;
+    if(m_osu->m_songBrowser2->isVisible() && m_osu->m_songBrowser2->getDatabase()->isFinished() == 1.f) {
+        can_scroll = false;
+    }
     if(m_osu->m_optionsMenu->isVisible()) can_scroll = false;
     if(m_osu->m_userStatsScreen->isVisible()) can_scroll = false;
     if(m_osu->m_changelog->isVisible()) can_scroll = false;

+ 4 - 0
src/Engine/Engine.cpp

@@ -105,6 +105,9 @@ Console *Engine::m_console = NULL;
 ConsoleBox *Engine::m_consoleBox = NULL;
 
 Engine::Engine(Environment *environment, const char *args) {
+    // XXX: run curl_global_cleanup() after waiting for network threads to terminate
+    curl_global_init(CURL_GLOBAL_DEFAULT);
+
     engine = this;
     m_environment = environment;
     env = environment;
@@ -527,6 +530,7 @@ void Engine::onShutdown() {
     if(m_bBlackout || (m_app != NULL && !m_app->onShutdown())) return;
 
     m_bBlackout = true;
+    m_sound->shutdown();
     m_environment->shutdown();
 }
 

+ 13 - 25
src/Engine/SoundEngine.cpp

@@ -319,22 +319,7 @@ void SoundEngine::updateOutputDevices(bool printInfo) {
 bool SoundEngine::initializeOutputDevice(OUTPUT_DEVICE device) {
     debugLog("SoundEngine: initializeOutputDevice( %s ) ...\n", device.name.toUtf8());
 
-    if(m_currentOutputDevice.driver == OutputDriver::BASS) {
-        BASS_SetDevice(m_currentOutputDevice.id);
-        BASS_Free();
-        BASS_SetDevice(0);
-    }
-
-#ifdef _WIN32
-    if(m_currentOutputDevice.driver == OutputDriver::BASS_ASIO) {
-        BASS_ASIO_Free();
-    } else if(m_currentOutputDevice.driver == OutputDriver::BASS_WASAPI) {
-        BASS_WASAPI_Free();
-    }
-#endif
-
-    g_bassOutputMixer = 0;
-    BASS_Free();  // free "No sound" device
+    shutdown();
 
     if(device.driver == OutputDriver::NONE || (device.driver == OutputDriver::BASS && device.id == 0)) {
         m_bReady = true;
@@ -504,23 +489,26 @@ bool SoundEngine::initializeOutputDevice(OUTPUT_DEVICE device) {
     return true;
 }
 
-SoundEngine::~SoundEngine() {
+void SoundEngine::restart() { setOutputDevice(m_currentOutputDevice); }
+
+void SoundEngine::shutdown() {
     if(m_currentOutputDevice.driver == OutputDriver::BASS) {
+        BASS_SetDevice(m_currentOutputDevice.id);
         BASS_Free();
-    } else if(m_currentOutputDevice.driver == OutputDriver::BASS_ASIO) {
+        BASS_SetDevice(0);
+    }
+
 #ifdef _WIN32
+    if(m_currentOutputDevice.driver == OutputDriver::BASS_ASIO) {
         BASS_ASIO_Free();
-#endif
-        BASS_Free();
     } else if(m_currentOutputDevice.driver == OutputDriver::BASS_WASAPI) {
-#ifdef _WIN32
         BASS_WASAPI_Free();
-#endif
-        BASS_Free();
     }
-}
+#endif
 
-void SoundEngine::restart() { setOutputDevice(m_currentOutputDevice); }
+    g_bassOutputMixer = 0;
+    BASS_Free();  // free "No sound" device
+}
 
 void SoundEngine::update() {}
 

+ 1 - 1
src/Engine/SoundEngine.h

@@ -29,9 +29,9 @@ struct OUTPUT_DEVICE {
 class SoundEngine {
    public:
     SoundEngine();
-    ~SoundEngine();
 
     void restart();
+    void shutdown();
 
     void update();
 

+ 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;