1
0

3 Коммитууд 4ad10055de ... 21a2857028

Эзэн SHA1 Мессеж Огноо
  Clément Wolf 21a2857028 Rewrite collections 3 долоо хоног өмнө
  Clément Wolf 864fa30454 Display friend names in red 3 долоо хоног өмнө
  Clément Wolf 9827a8ef44 Force exclusive mode on WASAPI output 3 долоо хоног өмнө

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

@@ -385,10 +385,12 @@ void handle_packet(Packet *packet) {
         read_int32(packet);
         // (nothing to do)
     } else if(packet->id == FRIENDS_LIST) {
+        friends.clear();
+
         uint16_t nb_friends = read_short(packet);
         for(int i = 0; i < nb_friends; i++) {
-            auto user = get_user_info(read_int32(packet));
-            user->is_friend = true;
+            uint32_t friend_id = read_int32(packet);
+            friends.push_back(friend_id);
         }
     } else if(packet->id == PROTOCOL_VERSION) {
         int protocol_version = read_int32(packet);

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

@@ -7,6 +7,7 @@
 #include "Bancho.h"
 #include "BanchoLeaderboard.h"
 #include "BanchoProtocol.h"
+#include "BanchoUsers.h"
 #include "CBaseUICheckbox.h"
 #include "ConVar.h"
 #include "Downloader.h"
@@ -96,6 +97,12 @@ void disconnect() {
     bancho.osu->m_optionsMenu->logInButton->is_loading = false;
     ConVars::sv_cheats.setValue(true);
 
+    for(auto pair : online_users) {
+        delete pair.second;
+    }
+    online_users.clear();
+    friends.clear();
+
     bancho.osu->m_chat->onDisconnect();
 
     // XXX: We should toggle between "offline" sorting options and "online" ones

+ 16 - 0
src/App/Osu/BanchoProtocol.cpp

@@ -187,6 +187,22 @@ std::string read_stdstring(Packet *packet) {
     return str_out;
 }
 
+MD5Hash read_hash(Packet *packet) {
+    MD5Hash hash;
+
+    uint8_t empty_check = read_byte(packet);
+    if(empty_check == 0) return hash;
+
+    uint32_t len = read_uleb128(packet);
+    if(len > 32) {
+        len = 32;
+    }
+
+    read_bytes(packet, (uint8_t *)hash.hash, len);
+    hash.hash[len] = '\0';
+    return hash;
+}
+
 void skip_string(Packet *packet) {
     uint8_t empty_check = read_byte(packet);
     if(empty_check == 0) {

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

@@ -254,6 +254,7 @@ double read_float64(Packet *packet);
 UString read_string(Packet *packet);
 std::string read_stdstring(Packet *packet);
 void skip_string(Packet *packet);
+MD5Hash read_hash(Packet *packet);
 
 void write_bytes(Packet *packet, uint8_t *bytes, size_t n);
 void write_byte(Packet *packet, uint8_t b);

+ 6 - 0
src/App/Osu/BanchoUsers.cpp

@@ -1,6 +1,7 @@
 #include "BanchoUsers.h"
 
 std::unordered_map<uint32_t, UserInfo*> online_users;
+std::vector<uint32_t> friends;
 
 UserInfo* get_user_info(uint32_t user_id, bool fetch) {
     auto it = online_users.find(user_id);
@@ -24,3 +25,8 @@ UserInfo* get_user_info(uint32_t user_id, bool fetch) {
 
     return info;
 }
+
+bool UserInfo::is_friend() {
+    auto it = std::find(friends.begin(), friends.end(), user_id);
+    return it != friends.end();
+}

+ 4 - 1
src/App/Osu/BanchoUsers.h

@@ -6,7 +6,6 @@
 
 struct UserInfo {
     uint32_t user_id = 0;
-    bool is_friend = false;
 
     // Presence (via USER_PRESENCE_REQUEST or USER_PRESENCE_REQUEST_ALL)
     UString name;
@@ -29,7 +28,11 @@ struct UserInfo {
     int32_t plays = 0;
     uint16_t pp = 0.f;
     float accuracy = 0.f;
+
+    bool is_friend();
 };
 
 extern std::unordered_map<uint32_t, UserInfo*> online_users;
+extern std::vector<uint32_t> friends;
+
 UserInfo* get_user_info(uint32_t user_id, bool fetch = false);

+ 233 - 0
src/App/Osu/Collections.cpp

@@ -0,0 +1,233 @@
+#include "Collections.h"
+
+#include "BanchoProtocol.h"
+#include "ConVar.h"
+#include "Engine.h"
+#include "OsuDatabase.h"
+
+bool collections_loaded = false;
+std::vector<Collection*> collections;
+
+void Collection::delete_collection() {
+    for(auto map : maps) {
+        remove_map(map);
+    }
+}
+
+void Collection::add_map(MD5Hash map_hash) {
+    {
+        auto it = std::find(deleted_maps.begin(), deleted_maps.end(), map_hash);
+        if(it != deleted_maps.end()) {
+            deleted_maps.erase(it);
+        }
+    }
+
+    {
+        auto it = std::find(neosu_maps.begin(), neosu_maps.end(), map_hash);
+        if(it == neosu_maps.end()) {
+            neosu_maps.push_back(map_hash);
+        }
+    }
+
+    {
+        auto it = std::find(maps.begin(), maps.end(), map_hash);
+        if(it == maps.end()) {
+            maps.push_back(map_hash);
+        }
+    }
+}
+
+void Collection::remove_map(MD5Hash map_hash) {
+    {
+        auto it = std::find(maps.begin(), maps.end(), map_hash);
+        if(it != maps.end()) {
+            maps.erase(it);
+        }
+    }
+
+    {
+        auto it = std::find(neosu_maps.begin(), neosu_maps.end(), map_hash);
+        if(it != neosu_maps.end()) {
+            neosu_maps.erase(it);
+        }
+    }
+
+    {
+        auto it = std::find(peppy_maps.begin(), peppy_maps.end(), map_hash);
+        if(it != peppy_maps.end()) {
+            deleted_maps.push_back(map_hash);
+        }
+    }
+}
+
+void Collection::rename_to(std::string new_name) {
+    if(new_name.length() < 1) new_name = "Untitled collection";
+    if(name == new_name) return;
+
+    auto new_collection = get_or_create_collection(new_name);
+
+    for(auto map : maps) {
+        remove_map(map);
+        new_collection->add_map(map);
+    }
+}
+
+Collection* get_or_create_collection(std::string name) {
+    if(name.length() < 1) name = "Untitled collection";
+
+    for(auto collection : collections) {
+        if(collection->name == name) {
+            return collection;
+        }
+    }
+
+    auto collection = new Collection();
+    collection->name = name;
+    collections.push_back(collection);
+
+    std::sort(collections.begin(), collections.end(), [](Collection* a, Collection* b) { return a->name < b->name; });
+
+    return collection;
+}
+
+bool load_collections() {
+    const double startTime = engine->getTimeReal();
+
+    unload_collections();
+
+    auto osu_folder = convar->getConVarByName("osu_folder")->getString();
+    auto osu_database_version = convar->getConVarByName("osu_database_version")->getInt();
+
+    std::string peppy_collections_path = osu_folder.toUtf8();
+    peppy_collections_path.append("collection.db");
+    Packet peppy_collections = load_db(peppy_collections_path);
+    if(peppy_collections.size > 0) {
+        uint32_t version = read_int32(&peppy_collections);
+        uint32_t nb_collections = read_int32(&peppy_collections);
+
+        if(version > osu_database_version) {
+            debugLog("osu!stable collection.db version more recent than neosu, loading might fail.\n");
+        }
+
+        for(int c = 0; c < nb_collections; c++) {
+            auto name = read_stdstring(&peppy_collections);
+            uint32_t nb_maps = read_int32(&peppy_collections);
+
+            auto collection = get_or_create_collection(name);
+            collection->maps.reserve(nb_maps);
+            collection->peppy_maps.reserve(nb_maps);
+
+            for(int m = 0; m < nb_maps; m++) {
+                auto map_hash = read_hash(&peppy_collections);
+                collection->maps.push_back(map_hash);
+                collection->peppy_maps.push_back(map_hash);
+            }
+        }
+    }
+    free(peppy_collections.memory);
+
+    auto neosu_collections = load_db("collections.db");
+    if(neosu_collections.size > 0) {
+        uint32_t version = read_int32(&neosu_collections);
+        uint32_t nb_collections = read_int32(&neosu_collections);
+
+        if(version > COLLECTIONS_DB_VERSION) {
+            debugLog("neosu collections.db version is too recent! Cannot load it without stuff breaking.\n");
+            free(neosu_collections.memory);
+            unload_collections();
+            return false;
+        }
+
+        for(int c = 0; c < nb_collections; c++) {
+            auto name = read_stdstring(&neosu_collections);
+            auto collection = get_or_create_collection(name);
+
+            uint32_t nb_deleted_maps = 0;
+            if(version >= 20240429) {
+                nb_deleted_maps = read_int32(&neosu_collections);
+            }
+
+            collection->deleted_maps.reserve(nb_deleted_maps);
+            for(int d = 0; d < nb_deleted_maps; d++) {
+                auto map_hash = read_hash(&neosu_collections);
+
+                auto it = std::find(collection->maps.begin(), collection->maps.end(), map_hash);
+                if(it != collection->maps.end()) {
+                    collection->maps.erase(it);
+                }
+
+                collection->deleted_maps.push_back(map_hash);
+            }
+
+            uint32_t nb_maps = read_int32(&neosu_collections);
+            collection->maps.reserve(collection->maps.size() + nb_maps);
+            collection->neosu_maps.reserve(nb_maps);
+
+            for(int m = 0; m < nb_maps; m++) {
+                auto map_hash = read_hash(&neosu_collections);
+
+                auto it = std::find(collection->maps.begin(), collection->maps.end(), map_hash);
+                if(it == collection->maps.end()) {
+                    collection->maps.push_back(map_hash);
+                }
+
+                collection->neosu_maps.push_back(map_hash);
+            }
+        }
+    }
+    free(neosu_collections.memory);
+
+    std::sort(collections.begin(), collections.end(), [](Collection* a, Collection* b) { return a->name < b->name; });
+
+    debugLog("collections.db: loading took %f seconds\n", (engine->getTimeReal() - startTime));
+    collections_loaded = true;
+    return true;
+}
+
+void unload_collections() {
+    collections_loaded = false;
+
+    for(auto collection : collections) {
+        delete collection;
+    }
+    collections.clear();
+}
+
+bool save_collections() {
+    if(!collections_loaded) {
+        debugLog("Cannot save collections since they weren't loaded properly first!\n");
+        return false;
+    }
+
+    const double startTime = engine->getTimeReal();
+
+    Packet db;
+    write_int32(&db, COLLECTIONS_DB_VERSION);
+
+    uint32_t nb_collections = collections.size();
+    write_int32(&db, nb_collections);
+
+    for(auto collection : collections) {
+        write_string(&db, collection->name.c_str());
+
+        uint32_t nb_deleted = collection->deleted_maps.size();
+        write_int32(&db, nb_deleted);
+        for(auto map : collection->deleted_maps) {
+            write_string(&db, map.hash);
+        }
+
+        uint32_t nb_neosu = collection->neosu_maps.size();
+        write_int32(&db, nb_neosu);
+        for(auto map : collection->neosu_maps) {
+            write_string(&db, map.hash);
+        }
+    }
+
+    if(!save_db(&db, "collections.db")) {
+        debugLog("Couldn't write collections.db!\n");
+        return false;
+    }
+
+    debugLog("collections.db: saving took %f seconds\n", (engine->getTimeReal() - startTime));
+    return true;
+}

+ 26 - 0
src/App/Osu/Collections.h

@@ -0,0 +1,26 @@
+#pragma once
+#include "UString.h"
+
+#define COLLECTIONS_DB_VERSION 20240429
+
+struct Collection {
+    std::string name;
+    std::vector<MD5Hash> maps;
+
+    std::vector<MD5Hash> neosu_maps;
+    std::vector<MD5Hash> peppy_maps;
+    std::vector<MD5Hash> deleted_maps;
+
+    void delete_collection();
+    void add_map(MD5Hash map_hash);
+    void remove_map(MD5Hash map_hash);
+    void rename_to(std::string new_name);
+};
+
+extern std::vector<Collection*> collections;
+
+Collection* get_or_create_collection(std::string name);
+
+bool load_collections();
+void unload_collections();
+bool save_collections();

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

@@ -384,7 +384,6 @@ Osu::Osu(int instanceID) {
         ->setCallback(fastdelegate::MakeDelegate(sound_engine, &SoundEngine::restart));
     convar->getConVarByName("win_snd_wasapi_buffer_size")->setCallback(_RESTART_SOUND_ENGINE_ON_CHANGE);
     convar->getConVarByName("win_snd_wasapi_period_size")->setCallback(_RESTART_SOUND_ENGINE_ON_CHANGE);
-    convar->getConVarByName("win_snd_wasapi_exclusive")->setCallback(_RESTART_SOUND_ENGINE_ON_CHANGE);
     convar->getConVarByName("asio_buffer_size")->setCallback(_RESTART_SOUND_ENGINE_ON_CHANGE);
 
     // Initialize skin after sound engine has started, or else sounds won't load properly

+ 7 - 3
src/App/Osu/OsuChangelog.cpp

@@ -40,8 +40,15 @@ OsuChangelog::OsuChangelog(Osu *osu) : OsuScreenBackable(osu) {
     latest.changes.push_back("- Added option to normalize loudness across songs");
     latest.changes.push_back("- Added server logo to main menu button");
     latest.changes.push_back("- Added instant_replay_duration convar");
+    latest.changes.push_back("- Added ability to remove beatmaps from osu!stable collections (only affects neosu)");
     latest.changes.push_back("- Allowed singleplayer cheats when the server doesn't accept score submissions");
+    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 collection processing");
+    latest.changes.push_back("- Removed support for the Nintendo Switch");
+    latest.changes.push_back("- Updated protocol version");
     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%");
@@ -51,9 +58,6 @@ OsuChangelog::OsuChangelog(Osu *osu) : OsuScreenBackable(osu) {
     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("- Disabled score submission when mods are toggled mid-game");
-    latest.changes.push_back("- Removed support for the Nintendo Switch");
-    latest.changes.push_back("- Updated protocol version");
     changelogs.push_back(latest);
 
     CHANGELOG v34_10;

+ 3 - 544
src/App/Osu/OsuDatabase.cpp

@@ -13,6 +13,7 @@
 
 #include "Bancho.h"  // md5
 #include "BanchoNetworking.h"
+#include "Collections.h"
 #include "ConVar.h"
 #include "Engine.h"
 #include "File.h"
@@ -249,15 +250,6 @@ struct SortScoreByPP : public OsuDatabase::SCORE_SORTING_COMPARATOR {
     }
 };
 
-struct SortCollectionByName {
-    bool operator()(OsuDatabase::Collection const &a, OsuDatabase::Collection const &b) {
-        // strict weak ordering!
-        if(a.name == b.name) return &a < &b;
-
-        return a.name.lessThanIgnoreCase(b.name);
-    }
-};
-
 class OsuDatabaseLoader : public Resource {
    public:
     OsuDatabaseLoader(OsuDatabase *db) : Resource() {
@@ -353,8 +345,6 @@ OsuDatabase::OsuDatabase(Osu *osu) {
     m_iVersion = 0;
     m_iFolderCount = 0;
 
-    m_bDidCollectionsChangeForSave = false;
-
     m_bScoresLoaded = false;
     m_bDidScoresChangeForSave = false;
     m_bDidScoresChangeForStats = true;
@@ -424,14 +414,7 @@ void OsuDatabase::update() {
                 debugLog("Refresh finished, added %i beatmaps in %f seconds.\n", m_databaseBeatmaps.size(),
                          m_importTimer->getElapsedTime());
 
-                // TODO: improve loading progress feedback here, currently we just freeze everything if this takes too
-                // long load custom collections after we have all beatmaps available (and m_rawHashToDiff2 +
-                // m_rawHashToBeatmap populated)
-                {
-                    loadCollections("collections.db", false, m_rawHashToDiff2, m_rawHashToBeatmap);
-
-                    std::sort(m_collections.begin(), m_collections.end(), SortCollectionByName());
-                }
+                load_collections();
 
                 m_fLoadingProgress = 1.0f;
 
@@ -444,8 +427,6 @@ void OsuDatabase::update() {
 }
 
 void OsuDatabase::load() {
-    m_bDidCollectionsChangeForSave = false;
-
     m_bInterruptLoad = false;
     m_fLoadingProgress = 0.0f;
 
@@ -464,7 +445,6 @@ void OsuDatabase::cancel() {
 
 void OsuDatabase::save() {
     saveScores();
-    saveCollections();
     saveStars();
 }
 
@@ -586,186 +566,6 @@ void OsuDatabase::sortScores(MD5Hash beatmapMD5Hash) {
     }
 }
 
-bool OsuDatabase::addCollection(UString collectionName) {
-    if(collectionName.length() < 1) return false;
-
-    // don't want duplicates
-    for(size_t i = 0; i < m_collections.size(); i++) {
-        if(m_collections[i].name == collectionName) return false;
-    }
-
-    Collection c;
-    {
-        c.isLegacyCollection = false;
-
-        c.name = collectionName;
-    }
-    m_collections.push_back(c);
-
-    std::sort(m_collections.begin(), m_collections.end(), SortCollectionByName());
-
-    m_bDidCollectionsChangeForSave = true;
-
-    if(osu_collections_save_immediately.getBool()) saveCollections();
-
-    return true;
-}
-
-bool OsuDatabase::renameCollection(UString oldCollectionName, UString newCollectionName) {
-    if(newCollectionName.length() < 1) return false;
-    if(oldCollectionName == newCollectionName) return false;
-
-    // don't want duplicates
-    for(size_t i = 0; i < m_collections.size(); i++) {
-        if(m_collections[i].name == newCollectionName) return false;
-    }
-
-    for(size_t i = 0; i < m_collections.size(); i++) {
-        if(m_collections[i].name == oldCollectionName) {
-            // can't rename loaded osu! collections
-            if(!m_collections[i].isLegacyCollection) {
-                m_collections[i].name = newCollectionName;
-
-                std::sort(m_collections.begin(), m_collections.end(), SortCollectionByName());
-
-                m_bDidCollectionsChangeForSave = true;
-
-                if(osu_collections_save_immediately.getBool()) saveCollections();
-
-                return true;
-            } else
-                return false;
-        }
-    }
-
-    return false;
-}
-
-void OsuDatabase::deleteCollection(UString collectionName) {
-    for(size_t i = 0; i < m_collections.size(); i++) {
-        if(m_collections[i].name == collectionName) {
-            // can't delete loaded osu! collections
-            if(!m_collections[i].isLegacyCollection) {
-                m_collections.erase(m_collections.begin() + i);
-
-                m_bDidCollectionsChangeForSave = true;
-
-                if(osu_collections_save_immediately.getBool()) saveCollections();
-            }
-
-            break;
-        }
-    }
-}
-
-void OsuDatabase::addBeatmapToCollection(UString collectionName, MD5Hash beatmapMD5Hash,
-                                         bool doSaveImmediatelyIfEnabled) {
-    for(size_t i = 0; i < m_collections.size(); i++) {
-        if(m_collections[i].name == collectionName) {
-            bool containedAlready = false;
-            for(size_t h = 0; h < m_collections[i].hashes.size(); h++) {
-                if(m_collections[i].hashes[h].hash == beatmapMD5Hash) {
-                    containedAlready = true;
-                    break;
-                }
-            }
-
-            if(!containedAlready) {
-                CollectionEntry entry;
-                {
-                    entry.isLegacyEntry = false;
-
-                    entry.hash = beatmapMD5Hash;
-                }
-                m_collections[i].hashes.push_back(entry);
-
-                m_bDidCollectionsChangeForSave = true;
-
-                if(doSaveImmediatelyIfEnabled && osu_collections_save_immediately.getBool()) saveCollections();
-
-                // also update .beatmaps for convenience (songbrowser will use that to rebuild the UI)
-                {
-                    OsuDatabaseBeatmap *beatmap = getBeatmap(beatmapMD5Hash);
-                    OsuDatabaseBeatmap *diff2 = getBeatmapDifficulty(beatmapMD5Hash);
-
-                    if(beatmap != NULL && diff2 != NULL) {
-                        bool beatmapContainedAlready = false;
-                        for(size_t b = 0; b < m_collections[i].beatmaps.size(); b++) {
-                            if(m_collections[i].beatmaps[b].first == beatmap) {
-                                beatmapContainedAlready = true;
-
-                                bool diffContainedAlready = false;
-                                for(size_t d = 0; d < m_collections[i].beatmaps[b].second.size(); d++) {
-                                    if(m_collections[i].beatmaps[b].second[d] == diff2) {
-                                        diffContainedAlready = true;
-                                        break;
-                                    }
-                                }
-
-                                if(!diffContainedAlready) m_collections[i].beatmaps[b].second.push_back(diff2);
-
-                                break;
-                            }
-                        }
-
-                        if(!beatmapContainedAlready) {
-                            std::vector<OsuDatabaseBeatmap *> diffs2;
-                            { diffs2.push_back(diff2); }
-                            m_collections[i].beatmaps.push_back(
-                                std::pair<OsuDatabaseBeatmap *, std::vector<OsuDatabaseBeatmap *>>(beatmap, diffs2));
-                        }
-                    }
-                }
-            }
-
-            break;
-        }
-    }
-}
-
-void OsuDatabase::removeBeatmapFromCollection(UString collectionName, MD5Hash beatmapMD5Hash,
-                                              bool doSaveImmediatelyIfEnabled) {
-    for(size_t i = 0; i < m_collections.size(); i++) {
-        if(m_collections[i].name == collectionName) {
-            bool didRemove = false;
-            for(size_t h = 0; h < m_collections[i].hashes.size(); h++) {
-                if(m_collections[i].hashes[h].hash == beatmapMD5Hash) {
-                    // can't delete loaded osu! collection entries
-                    if(!m_collections[i].hashes[h].isLegacyEntry) {
-                        m_collections[i].hashes.erase(m_collections[i].hashes.begin() + h);
-
-                        didRemove = true;
-
-                        m_bDidCollectionsChangeForSave = true;
-
-                        if(doSaveImmediatelyIfEnabled && osu_collections_save_immediately.getBool()) saveCollections();
-                    }
-
-                    break;
-                }
-            }
-
-            // also update .beatmaps for convenience (songbrowser will use that to rebuild the UI)
-            if(didRemove) {
-                for(size_t b = 0; b < m_collections[i].beatmaps.size(); b++) {
-                    bool found = false;
-                    for(size_t d = 0; d < m_collections[i].beatmaps[b].second.size(); d++) {
-                        if(m_collections[i].beatmaps[b].second[d]->getMD5Hash() == beatmapMD5Hash) {
-                            found = true;
-
-                            m_collections[i].beatmaps[b].second.erase(m_collections[i].beatmaps[b].second.begin() + d);
-
-                            break;
-                        }
-                    }
-
-                    if(found) break;
-                }
-            }
-        }
-    }
-}
-
 std::vector<UString> OsuDatabase::getPlayerNamesWithPPScores() {
     std::vector<MD5Hash> keys;
     keys.reserve(m_scores.size());
@@ -1138,9 +938,6 @@ void OsuDatabase::scheduleLoadRaw() {
 }
 
 void OsuDatabase::loadDB(Packet *db, bool &fallbackToRawLoad) {
-    // reset
-    m_collections.clear();
-
     if(m_databaseBeatmaps.size() > 0) debugLog("WARNING: OsuDatabase::loadDB() called without cleared m_beatmaps!!!\n");
 
     m_databaseBeatmaps.clear();
@@ -1688,17 +1485,7 @@ void OsuDatabase::loadDB(Packet *db, bool &fallbackToRawLoad) {
     // signal that we are almost done
     m_fLoadingProgress = 0.75f;
 
-    // load legacy collection.db
-    if(osu_collections_legacy_enabled.getBool()) {
-        std::string legacyCollectionFilePath = osu_folder.getString().toUtf8();
-        legacyCollectionFilePath.append("collection.db");
-        loadCollections(legacyCollectionFilePath, true, hashToDiff2, hashToBeatmap);
-    }
-
-    // load custom collections.db (after having loaded legacy!)
-    if(osu_collections_custom_enabled.getBool()) loadCollections("collections.db", false, hashToDiff2, hashToBeatmap);
-
-    std::sort(m_collections.begin(), m_collections.end(), SortCollectionByName());
+    load_collections();
 
     // signal that we are done
     m_fLoadingProgress = 1.0f;
@@ -2150,334 +1937,6 @@ void OsuDatabase::saveScores() {
     debugLog("Took %f seconds.\n", (engine->getTimeReal() - startTime));
 }
 
-void OsuDatabase::loadCollections(std::string collectionFilePath, bool isLegacy,
-                                  const std::unordered_map<MD5Hash, OsuDatabaseBeatmap *> &hashToDiff2,
-                                  const std::unordered_map<MD5Hash, OsuDatabaseBeatmap *> &hashToBeatmap) {
-    bool wasInterrupted = false;
-
-    struct CollectionLoadingHelper {
-        static void addBeatmapsEntryForBeatmapAndDiff2(Collection &c, OsuDatabaseBeatmap *beatmap,
-                                                       OsuDatabaseBeatmap *diff2, std::atomic<bool> &interruptLoad,
-                                                       bool &wasInterrupted) {
-            if(beatmap == NULL || diff2 == NULL) return;
-
-            // we now have one matching OsuBeatmap and OsuBeatmapDifficulty, add either of them if they don't exist yet
-            bool beatmapIsAlreadyInCollection = false;
-            {
-                for(int m = 0; m < c.beatmaps.size(); m++) {
-                    if(interruptLoad.load()) {
-                        wasInterrupted = true;
-                        break;
-                    }  // cancellation point
-
-                    if(c.beatmaps[m].first == beatmap) {
-                        beatmapIsAlreadyInCollection = true;
-
-                        // the beatmap already exists, check if we have to add the current diff
-                        bool diffIsAlreadyInCollection = false;
-                        {
-                            for(int d = 0; d < c.beatmaps[m].second.size(); d++) {
-                                if(interruptLoad.load()) {
-                                    wasInterrupted = true;
-                                    break;
-                                }  // cancellation point
-
-                                if(c.beatmaps[m].second[d] == diff2) {
-                                    diffIsAlreadyInCollection = true;
-                                    break;
-                                }
-                            }
-                        }
-                        if(!diffIsAlreadyInCollection) c.beatmaps[m].second.push_back(diff2);
-
-                        break;
-                    }
-                }
-            }
-            if(!beatmapIsAlreadyInCollection) {
-                std::vector<OsuDatabaseBeatmap *> diffs2;
-                { diffs2.push_back(diff2); }
-                c.beatmaps.push_back(
-                    std::pair<OsuDatabaseBeatmap *, std::vector<OsuDatabaseBeatmap *>>(beatmap, diffs2));
-            }
-        }
-    };
-
-    Packet collectionFile = load_db(collectionFilePath);
-    if(collectionFile.size > 0) {
-        const int version = read_int32(&collectionFile);
-        const int numCollections = read_int32(&collectionFile);
-
-        debugLog("Collection: version = %i, numCollections = %i\n", version, numCollections);
-
-        const bool isLegacyAndVersionValid =
-            (isLegacy && (version <= osu_database_version.getInt() || osu_database_ignore_version.getBool()));
-        const bool isCustomAndVersionValid = (!isLegacy && (version <= osu_collections_custom_version.getInt()));
-
-        if(isLegacyAndVersionValid || isCustomAndVersionValid) {
-            for(int i = 0; i < numCollections; i++) {
-                if(m_bInterruptLoad.load()) {
-                    wasInterrupted = true;
-                    break;
-                }  // cancellation point
-
-                m_fLoadingProgress = 0.75f + 0.24f * ((float)(i + 1) / (float)numCollections);
-
-                auto name = read_string(&collectionFile);
-                const int numBeatmaps = read_int32(&collectionFile);
-
-                if(Osu::debug->getBool())
-                    debugLog("Raw Collection #%i: name = %s, numBeatmaps = %i\n", i, name.toUtf8(), numBeatmaps);
-
-                Collection c;
-                c.isLegacyCollection = isLegacy;
-                c.name = name;
-
-                for(int b = 0; b < numBeatmaps; b++) {
-                    if(m_bInterruptLoad.load()) {
-                        wasInterrupted = true;
-                        break;
-                    }  // cancellation point
-
-                    auto hash_str = read_stdstring(&collectionFile);
-                    MD5Hash md5hash = hash_str.c_str();
-
-                    CollectionEntry entry = {
-                        .isLegacyEntry = isLegacy,
-                        .hash = md5hash,
-                    };
-                    c.hashes.push_back(entry);
-                }
-
-                if(c.hashes.size() > 0) {
-                    // collect OsuBeatmaps corresponding to this collection
-
-                    // go through every hash of the collection
-                    std::vector<OsuDatabaseBeatmap *> matchingDiffs2;
-                    for(int h = 0; h < c.hashes.size(); h++) {
-                        if(m_bInterruptLoad.load()) {
-                            wasInterrupted = true;
-                            break;
-                        }  // cancellation point
-
-                        // new: use hashmap
-                        const auto result = hashToDiff2.find(c.hashes[h].hash);
-                        if(result != hashToDiff2.end()) matchingDiffs2.push_back(result->second);
-                    }
-
-                    // we now have an array of all OsuBeatmapDifficulty objects within this collection
-
-                    // go through every found OsuBeatmapDifficulty
-                    for(int md = 0; md < matchingDiffs2.size(); md++) {
-                        if(m_bInterruptLoad.load()) {
-                            wasInterrupted = true;
-                            break;
-                        }  // cancellation point
-
-                        OsuDatabaseBeatmap *diff2 = matchingDiffs2[md];
-
-                        if(diff2 == NULL) continue;
-
-                        // find the OsuBeatmap object corresponding to this diff
-                        OsuDatabaseBeatmap *beatmap = NULL;
-                        // new: use hashmap
-                        const auto result = hashToBeatmap.find(diff2->getMD5Hash());
-                        if(result != hashToBeatmap.end()) beatmap = result->second;
-
-                        CollectionLoadingHelper::addBeatmapsEntryForBeatmapAndDiff2(c, beatmap, diff2, m_bInterruptLoad,
-                                                                                    wasInterrupted);
-                    }
-                }
-
-                // add the collection
-                // check if we already have a collection with that name, if so then just add our new entries to it
-                // (necessary since this function will load both osu!'s collection.db as well as our own custom
-                // collections.db) this handles all the merging between both legacy and custom collections
-                {
-                    bool collectionAlreadyExists = false;
-                    for(size_t cc = 0; cc < m_collections.size(); cc++) {
-                        if(m_bInterruptLoad.load()) {
-                            wasInterrupted = true;
-                            break;
-                        }  // cancellation point
-
-                        Collection &existingCollection = m_collections[cc];
-                        if(existingCollection.name == c.name) {
-                            collectionAlreadyExists = true;
-
-                            // merge with existing collection
-                            {
-                                for(size_t e = 0; e < c.hashes.size(); e++) {
-                                    const MD5Hash &toBeAddedEntryHash = c.hashes[e].hash;
-
-                                    bool entryAlreadyExists = false;
-                                    {
-                                        for(size_t ee = 0; ee < existingCollection.hashes.size(); ee++) {
-                                            const MD5Hash &existingEntryHash = existingCollection.hashes[ee].hash;
-                                            if(existingEntryHash == toBeAddedEntryHash) {
-                                                entryAlreadyExists = true;
-                                                break;
-                                            }
-                                        }
-                                    }
-
-                                    // if the entry does not yet exist in the existing collection, then add that entry
-                                    if(!entryAlreadyExists) {
-                                        // add to .hashes
-                                        {
-                                            CollectionEntry toBeAddedEntry;
-                                            {
-                                                toBeAddedEntry.isLegacyEntry = isLegacy;
-
-                                                toBeAddedEntry.hash = toBeAddedEntryHash;
-                                            }
-                                            existingCollection.hashes.push_back(toBeAddedEntry);
-                                        }
-
-                                        // add to .beatmaps
-                                        {
-                                            const auto result = hashToDiff2.find(toBeAddedEntryHash);
-                                            if(result != hashToDiff2.end()) {
-                                                OsuDatabaseBeatmap *diff2 = result->second;
-
-                                                if(diff2 != NULL) {
-                                                    // find the OsuBeatmap object corresponding to this diff
-                                                    OsuDatabaseBeatmap *beatmap = NULL;
-                                                    // new: use hashmap
-                                                    const auto result = hashToBeatmap.find(diff2->getMD5Hash());
-                                                    if(result != hashToBeatmap.end()) beatmap = result->second;
-
-                                                    CollectionLoadingHelper::addBeatmapsEntryForBeatmapAndDiff2(
-                                                        existingCollection, beatmap, diff2, m_bInterruptLoad,
-                                                        wasInterrupted);
-                                                }
-                                            }
-                                        }
-                                    }
-                                }
-                            }
-
-                            break;
-                        }
-                    }
-                    if(!collectionAlreadyExists) m_collections.push_back(c);
-                }
-            }
-        } else
-            m_osu->getNotificationOverlay()->addNotification(
-                UString::format("collection.db version unknown (%i),  skipping loading.", version), 0xffffff00, false,
-                5.0f);
-
-        if(Osu::debug->getBool()) {
-            for(int i = 0; i < m_collections.size(); i++) {
-                debugLog("Collection #%i: name = %s, numBeatmaps = %i\n", i, m_collections[i].name.toUtf8(),
-                         m_collections[i].beatmaps.size());
-            }
-        }
-    } else {
-        debugLog("OsuBeatmapDatabase::loadDB() : Couldn't load %s\n", collectionFilePath.c_str());
-    }
-
-    // backup
-    if(!isLegacy) {
-        File originalCollectionsFile(collectionFilePath);
-        if(originalCollectionsFile.canRead()) {
-            std::string backupCollectionsFilePath = collectionFilePath;
-            const int forcedBackupCounter = 3;
-            backupCollectionsFilePath.append(
-                UString::format(".%i_%i.backup", osu_collections_custom_version.getInt(), forcedBackupCounter));
-
-            if(!env->fileExists(backupCollectionsFilePath))  // NOTE: avoid overwriting when people switch betas
-            {
-                File backupCollectionsFile(backupCollectionsFilePath, File::TYPE::WRITE);
-                if(backupCollectionsFile.canWrite()) {
-                    const uint8_t *originalCollectionsFileBytes = originalCollectionsFile.readFile();
-                    if(originalCollectionsFileBytes != NULL)
-                        backupCollectionsFile.write(originalCollectionsFileBytes,
-                                                    originalCollectionsFile.getFileSize());
-                }
-            }
-        }
-    }
-
-    // don't keep partially loaded collections. the user should notice at that point that it's not a good idea to edit
-    // collections after having interrupted loading.
-    if(wasInterrupted) m_collections.clear();
-}
-
-void OsuDatabase::saveCollections() {
-    if(!m_bDidCollectionsChangeForSave) return;
-    m_bDidCollectionsChangeForSave = false;
-
-    const int32_t dbVersion = osu_collections_custom_version.getInt();
-
-    if(m_collections.size() > 0) {
-        debugLog("Osu: Saving collections ...\n");
-
-        Packet db;
-        const double startTime = engine->getTimeReal();
-
-        // check how much we actually have to save
-        // note that we are only saving non-legacy collections and entries (i.e. things which were added/deleted inside
-        // neosu) reason being that it is more annoying to have osu!-side collections modifications be completely
-        // ignored (because we would make a full copy initially) if a collection or entry is deleted in osu!, then you
-        // would expect it to also be deleted here but, if a collection or entry is added in neosu, then deleting the
-        // collection in osu! should only delete all osu!-side entries
-        int32_t numNonLegacyCollectionsOrCollectionsWithNonLegacyEntries = 0;
-        for(size_t c = 0; c < m_collections.size(); c++) {
-            if(!m_collections[c].isLegacyCollection)
-                numNonLegacyCollectionsOrCollectionsWithNonLegacyEntries++;
-            else {
-                // does this legacy collection have any non-legacy entries?
-                for(size_t h = 0; h < m_collections[c].hashes.size(); h++) {
-                    if(!m_collections[c].hashes[h].isLegacyEntry) {
-                        numNonLegacyCollectionsOrCollectionsWithNonLegacyEntries++;
-                        break;
-                    }
-                }
-            }
-        }
-
-        write_int32(&db, dbVersion);
-        write_int32(&db, numNonLegacyCollectionsOrCollectionsWithNonLegacyEntries);
-
-        if(numNonLegacyCollectionsOrCollectionsWithNonLegacyEntries > 0) {
-            for(size_t c = 0; c < m_collections.size(); c++) {
-                bool hasNonLegacyEntries = false;
-                {
-                    for(size_t h = 0; h < m_collections[c].hashes.size(); h++) {
-                        if(!m_collections[c].hashes[h].isLegacyEntry) {
-                            hasNonLegacyEntries = true;
-                            break;
-                        }
-                    }
-                }
-
-                if(!m_collections[c].isLegacyCollection || hasNonLegacyEntries) {
-                    int32_t numNonLegacyEntries = 0;
-                    for(size_t h = 0; h < m_collections[c].hashes.size(); h++) {
-                        if(!m_collections[c].hashes[h].isLegacyEntry) numNonLegacyEntries++;
-                    }
-
-                    write_string(&db, m_collections[c].name);
-                    write_int32(&db, numNonLegacyEntries);
-
-                    for(size_t h = 0; h < m_collections[c].hashes.size(); h++) {
-                        if(!m_collections[c].hashes[h].isLegacyEntry)
-                            write_string(&db, m_collections[c].hashes[h].hash.hash);
-                    }
-                }
-            }
-        }
-
-        if(!save_db(&db, "collections.db")) {
-            debugLog("Couldn't write collections.db!\n");
-        }
-
-        debugLog("Took %f seconds.\n", (engine->getTimeReal() - startTime));
-    }
-}
-
 OsuDatabaseBeatmap *OsuDatabase::loadRawBeatmap(std::string beatmapPath) {
     if(Osu::debug->getBool()) debugLog("OsuBeatmapDatabase::loadRawBeatmap() : %s\n", beatmapPath.c_str());
 

+ 1 - 43
src/App/Osu/OsuDatabase.h

@@ -1,13 +1,4 @@
-//================ Copyright (c) 2016, PG, All rights reserved. =================//
-//
-// Purpose:		osu!.db + collection.db + raw loader + scores etc.
-//
-// $NoKeywords: $osubdb
-//===============================================================================//
-
-#ifndef OSUDATABASE_H
-#define OSUDATABASE_H
-
+#pragma once
 #include "BanchoProtocol.h"  // Packet
 #include "OsuReplay.h"
 #include "OsuScore.h"
@@ -35,18 +26,6 @@ bool save_db(Packet *db, std::string path);
 
 class OsuDatabase {
    public:
-    struct CollectionEntry {
-        bool isLegacyEntry;  // used for identifying loaded osu! collection entries
-        MD5Hash hash;
-    };
-
-    struct Collection {
-        bool isLegacyCollection;  // used for identifying loaded osu! collections
-        UString name;
-        std::vector<CollectionEntry> hashes;
-        std::vector<std::pair<OsuDatabaseBeatmap *, std::vector<OsuDatabaseBeatmap *>>> beatmaps;
-    };
-
     struct PlayerStats {
         UString name;
         float pp;
@@ -90,14 +69,6 @@ class OsuDatabase {
     void forceScoreUpdateOnNextCalculatePlayerStats() { m_bDidScoresChangeForStats = true; }
     void forceScoresSaveOnNextShutdown() { m_bDidScoresChangeForSave = true; }
 
-    bool addCollection(UString collectionName);
-    bool renameCollection(UString oldCollectionName, UString newCollectionName);
-    void deleteCollection(UString collectionName);
-    void addBeatmapToCollection(UString collectionName, MD5Hash beatmapMD5Hash, bool doSaveImmediatelyIfEnabled = true);
-    void removeBeatmapFromCollection(UString collectionName, MD5Hash beatmapMD5Hash,
-                                     bool doSaveImmediatelyIfEnabled = true);
-    void triggerSaveCollections() { saveCollections(); }
-
     std::vector<UString> getPlayerNamesWithPPScores();
     std::vector<UString> getPlayerNamesWithScoresForUserSwitcher();
     PlayerPPScores getPlayerPPScores(UString playerName);
@@ -115,8 +86,6 @@ class OsuDatabase {
     OsuDatabaseBeatmap *getBeatmap(const MD5Hash &md5hash);
     OsuDatabaseBeatmap *getBeatmapDifficulty(const MD5Hash &md5hash);
 
-    inline const std::vector<Collection> &getCollections() const { return m_collections; }
-
     inline std::unordered_map<MD5Hash, std::vector<Score>> *getScores() { return &m_scores; }
     inline const std::vector<SCORE_SORTING_METHOD> &getScoreSortingMethods() const { return m_scoreSortingMethods; }
 
@@ -145,11 +114,6 @@ class OsuDatabase {
     void loadScores();
     void saveScores();
 
-    void loadCollections(std::string collectionFilePath, bool isLegacy,
-                         const std::unordered_map<MD5Hash, OsuDatabaseBeatmap *> &hashToDiff2,
-                         const std::unordered_map<MD5Hash, OsuDatabaseBeatmap *> &hashToBeatmap);
-    void saveCollections();
-
     void onScoresRename(UString args);
     void onScoresExport();
 
@@ -168,10 +132,6 @@ class OsuDatabase {
     int m_iVersion;
     int m_iFolderCount;
 
-    // collection.db (legacy and custom)
-    std::vector<Collection> m_collections;
-    bool m_bDidCollectionsChangeForSave;
-
     // scores.db (legacy and custom)
     bool m_bScoresLoaded;
     std::unordered_map<MD5Hash, std::vector<Score>> m_scores;
@@ -196,5 +156,3 @@ class OsuDatabase {
     };
     std::unordered_map<MD5Hash, STARS_CACHE_ENTRY> m_starsCache;
 };
-
-#endif

+ 0 - 3
src/App/Osu/OsuOptionsMenu.cpp

@@ -733,9 +733,6 @@ OsuOptionsMenu::OsuOptionsMenu(Osu *osu) : OsuScreenBackable(osu) {
             addLabel("Windows 10: Start at 1 ms,")->setTextColor(0xff666666);
             addLabel("and if crackling: increment until fixed.")->setTextColor(0xff666666);
             addLabel("(lower is better, non-wasapi has ~40 ms minimum)")->setTextColor(0xff666666);
-            addCheckbox("Exclusive Mode",
-                        "Dramatically reduces latency, but prevents other applications from capturing/playing audio.",
-                        convar->getConVarByName("win_snd_wasapi_exclusive"));
             addLabel("");
             addLabel("");
             addLabel("WARNING: Only if you know what you are doing")->setTextColor(0xffff0000);

+ 5 - 1
src/App/Osu/OsuScoreboardSlot.cpp

@@ -2,6 +2,7 @@
 
 #include "AnimationHandler.h"
 #include "Bancho.h"
+#include "BanchoUsers.h"
 #include "Engine.h"
 #include "Font.h"
 #include "Osu.h"
@@ -12,6 +13,9 @@ OsuScoreboardSlot::OsuScoreboardSlot(SCORE_ENTRY score, int index) {
     m_avatar = new OsuUIAvatar(score.player_id, 0, 0, 0, 0);
     m_score = score;
     m_index = index;
+
+    auto user = get_user_info(score.player_id);
+    is_friend = user->is_friend();
 }
 
 OsuScoreboardSlot::~OsuScoreboardSlot() {
@@ -55,7 +59,7 @@ void OsuScoreboardSlot::draw(Graphics *g) {
     } else if(m_index == 0) {
         g->setColor(0xff1b6a8c);
     } else {
-        g->setColor(0xff114459);
+        g->setColor(is_friend ? 0xff9C205C : 0xff114459);
     }
     g->setAlpha(0.3f * m_fAlpha);
 

+ 1 - 0
src/App/Osu/OsuScoreboardSlot.h

@@ -16,5 +16,6 @@ struct OsuScoreboardSlot {
     float m_y = 0.f;
     float m_fAlpha = 0.f;
     float m_fFlash = 0.f;
+    bool is_friend = false;
     bool was_visible = false;
 };

+ 86 - 107
src/App/Osu/OsuSongBrowser.cpp

@@ -12,6 +12,7 @@
 #include "CBaseUIImageButton.h"
 #include "CBaseUILabel.h"
 #include "CBaseUIScrollView.h"
+#include "Collections.h"
 #include "ConVar.h"
 #include "Engine.h"
 #include "Keyboard.h"
@@ -1697,6 +1698,7 @@ void OsuSongBrowser::refreshBeatmaps() {
         delete m_songButtons[i];
     }
     m_songButtons.clear();
+    hashToSongButton.clear();
     for(size_t i = 0; i < m_collectionButtons.size(); i++) {
         delete m_collectionButtons[i];
     }
@@ -1755,15 +1757,19 @@ void OsuSongBrowser::addBeatmap(OsuDatabaseBeatmap *beatmap) {
     if(beatmap->getDifficulties().size() < 1) return;
 
     OsuUISongBrowserSongButton *songButton;
-    if(beatmap->getDifficulties().size() > 1)
+    if(beatmap->getDifficulties().size() > 1) {
         songButton = new OsuUISongBrowserSongButton(m_osu, this, m_songBrowser, m_contextMenu, 250,
                                                     250 + m_beatmaps.size() * 50, 200, 50, "", beatmap);
-    else
+    } else {
         songButton = new OsuUISongBrowserSongDifficultyButton(m_osu, this, m_songBrowser, m_contextMenu, 250,
                                                               250 + m_beatmaps.size() * 50, 200, 50, "",
                                                               beatmap->getDifficulties()[0], NULL);
+    }
 
     m_songButtons.push_back(songButton);
+    for(auto diff : beatmap->getDifficulties()) {
+        hashToSongButton[diff->getMD5Hash()] = songButton;
+    }
 
     // prebuild temporary list of all relevant buttons, used by some groups
     std::vector<OsuUISongBrowserButton *> tempChildrenForGroups;
@@ -3853,22 +3859,21 @@ void OsuSongBrowser::onSongButtonContextMenu(OsuUISongBrowserSongButton *songBut
     {
         if(id == 1) {
             // add diff to collection
-
-            m_db->addBeatmapToCollection(text, songButton->getDatabaseBeatmap()->getMD5Hash());
-
+            std::string name = text.toUtf8();
+            auto collection = get_or_create_collection(name);
+            collection->add_map(songButton->getDatabaseBeatmap()->getMD5Hash());
+            save_collections();
             updateUIScheduled = true;
         } else if(id == 2) {
             // add set to collection
-
+            std::string name = text.toUtf8();
+            auto collection = get_or_create_collection(name);
             const std::vector<MD5Hash> beatmapSetHashes =
                 CollectionManagementHelper::getBeatmapSetHashesForSongButton(songButton, m_db);
-            for(size_t i = 0; i < beatmapSetHashes.size(); i++) {
-                m_db->addBeatmapToCollection(text, beatmapSetHashes[i],
-                                             false);  // (don't save here every time) (this will ignore already added
-                                                      // parts of the set, so nothing to worry about)
+            for(auto hash : beatmapSetHashes) {
+                collection->add_map(hash);
             }
-            m_db->triggerSaveCollections();  // (but do save here once at the end)
-
+            save_collections();
             updateUIScheduled = true;
         } else if(id == 3) {
             // remove diff from collection
@@ -3884,8 +3889,10 @@ void OsuSongBrowser::onSongButtonContextMenu(OsuUISongBrowserSongButton *songBut
                 }
             }
 
-            m_db->removeBeatmapFromCollection(collectionName, songButton->getDatabaseBeatmap()->getMD5Hash());
-
+            std::string name = collectionName.toUtf8();
+            auto collection = get_or_create_collection(name);
+            collection->remove_map(songButton->getDatabaseBeatmap()->getMD5Hash());
+            save_collections();
             updateUIScheduled = true;
         } else if(id == 4) {
             // remove entire set from collection
@@ -3901,41 +3908,35 @@ void OsuSongBrowser::onSongButtonContextMenu(OsuUISongBrowserSongButton *songBut
                 }
             }
 
+            std::string name = collectionName.toUtf8();
+            auto collection = get_or_create_collection(name);
             const std::vector<MD5Hash> beatmapSetHashes =
                 CollectionManagementHelper::getBeatmapSetHashesForSongButton(songButton, m_db);
-            for(size_t i = 0; i < beatmapSetHashes.size(); i++) {
-                m_db->removeBeatmapFromCollection(collectionName, beatmapSetHashes[i],
-                                                  false);  // (don't save here every time)
+            for(auto hash : beatmapSetHashes) {
+                collection->remove_map(hash);
             }
-            m_db->triggerSaveCollections();  // (but do save here once at the end)
-
+            save_collections();
             updateUIScheduled = true;
         } else if(id == -2 || id == -4) {
-            // add new collection with name text
-
-            if(!m_db->addCollection(text))
-                m_osu->getNotificationOverlay()->addNotification("Error: Collection name already exists or is invalid.",
-                                                                 0xffffff00);
-            else {
-                if(id == -2) {
-                    // id == -2 means add diff to the just-created new collection
-
-                    m_db->addBeatmapToCollection(text, songButton->getDatabaseBeatmap()->getMD5Hash());
-
-                    updateUIScheduled = true;
-                } else if(id == -4) {
-                    // id == -4 means add set to the just-created new collection
-
-                    const std::vector<MD5Hash> beatmapSetHashes =
-                        CollectionManagementHelper::getBeatmapSetHashesForSongButton(songButton, m_db);
-                    for(size_t i = 0; i < beatmapSetHashes.size(); i++) {
-                        m_db->addBeatmapToCollection(text, beatmapSetHashes[i], false);  // (don't save here every time)
-                    }
-                    m_db->triggerSaveCollections();  // (but do save here once at the end)
-
-                    updateUIScheduled = true;
+            // add beatmap(set) to new collection
+            std::string name = text.toUtf8();
+            auto collection = get_or_create_collection(name);
+
+            if(id == -2) {
+                // id == -2 means beatmap
+                collection->add_map(songButton->getDatabaseBeatmap()->getMD5Hash());
+                updateUIScheduled = true;
+            } else if(id == -4) {
+                // id == -4 means beatmapset
+                const std::vector<MD5Hash> beatmapSetHashes =
+                    CollectionManagementHelper::getBeatmapSetHashesForSongButton(songButton, m_db);
+                for(size_t i = 0; i < beatmapSetHashes.size(); i++) {
+                    collection->add_map(beatmapSetHashes[i]);
                 }
+                updateUIScheduled = true;
             }
+
+            save_collections();
         }
     }
 
@@ -3976,7 +3977,10 @@ void OsuSongBrowser::onCollectionButtonContextMenu(OsuUISongBrowserCollectionBut
                 // reset UI state
                 m_selectionPreviousCollectionButton = NULL;
 
-                m_db->deleteCollection(text);
+                std::string name = text.toUtf8();
+                auto collection = get_or_create_collection(name);
+                collection->delete_collection();
+                save_collections();
 
                 // update UI
                 { onGroupCollections(false); }
@@ -4167,77 +4171,52 @@ void OsuSongBrowser::recreateCollectionsButtons() {
     Timer t;
     t.start();
 
-    const std::vector<OsuDatabase::Collection> &collections = m_db->getCollections();
-
-    std::vector<std::vector<OsuUISongBrowserButton *>> collection_button_children;
-    for(int i = 0; i < collections.size(); i++) {
-        collection_button_children.push_back(std::vector<OsuUISongBrowserButton *>());
-    }
-
-    for(auto &song_button : m_songButtons) {
-        for(int i = 0; i < collections.size(); i++) {
-            for(auto &pair : collections[i].beatmaps) {
-                const OsuDatabaseBeatmap *collectionBeatmap = pair.first;
-                const std::vector<OsuDatabaseBeatmap *> &colDiffs = pair.second;
-
-                // first: search direct buttons on collection beatmap
-                auto song_beatmap = song_button->getDatabaseBeatmap();
-                bool isMatchingSongButton = (song_beatmap == collectionBeatmap);
-
-                // second: search direct buttons on collection diffs (e.g. standalone diffs which still have a wrapper
-                // beatmap)
-                if(!isMatchingSongButton) {
-                    for(auto &diff : colDiffs) {
-                        if(song_beatmap == diff) {
-                            isMatchingSongButton = true;
-                            break;
-                        }
-                    }
-                }
-
-                // third: search child buttons
-                if(!isMatchingSongButton) {
-                    const std::vector<OsuUISongBrowserButton *> &songButtonChildren = song_button->getChildren();
-                    for(OsuUISongBrowserButton *sbc : songButtonChildren) {
-                        // check set match
-                        if(sbc->getDatabaseBeatmap() == collectionBeatmap) {
-                            isMatchingSongButton = true;
-                            break;
-                        }
+    for(auto collection : collections) {
+        if(collection->maps.empty()) continue;
+
+        std::vector<OsuUISongBrowserButton *> folder;
+        std::vector<uint32_t> matched_sets;
+
+        for(auto &map : collection->maps) {
+            auto it = hashToSongButton.find(map);
+            if(it == hashToSongButton.end()) continue;
+
+            uint32_t set_id = 0;
+            auto song_button = it->second;
+            const std::vector<OsuUISongBrowserButton *> &songButtonChildren = song_button->getChildren();
+            std::vector<OsuUISongBrowserButton *> matching_diffs;
+            for(OsuUISongBrowserButton *sbc : songButtonChildren) {
+                for(auto &map2 : collection->maps) {
+                    if(sbc->getDatabaseBeatmap()->getMD5Hash() == map2) {
+                        matching_diffs.push_back(sbc);
+                        set_id = sbc->getDatabaseBeatmap()->getSetID();
                     }
                 }
+            }
 
-                if(isMatchingSongButton) {
-                    const std::vector<OsuUISongBrowserButton *> &songButtonChildren = song_button->getChildren();
-                    std::vector<OsuUISongBrowserButton *> matchingDiffs;
-
-                    for(OsuUISongBrowserButton *sbc : songButtonChildren) {
-                        for(auto &diff : colDiffs) {
-                            if(sbc->getDatabaseBeatmap() == diff) matchingDiffs.push_back(sbc);
-                        }
-                    }
+            auto set = std::find(matched_sets.begin(), matched_sets.end(), set_id);
+            if(set == matched_sets.end()) {
+                // Mark set as processed so we don't add the diffs from the same set twice
+                matched_sets.push_back(set_id);
+            } else {
+                // We already added the maps from this set to the collection!
+                continue;
+            }
 
-                    // new: only add matched diff buttons, instead of the set button
-                    // however, if all diffs match, then add the set button instead (user added all diffs of beatmap
-                    // into collection)
-                    if(songButtonChildren.size() == matchingDiffs.size())
-                        collection_button_children[i].push_back(
-                            song_button);  // TODO: this is more of a convenience workaround until dynamic regrouping is
-                                           // implemented
-                    else
-                        collection_button_children[i].insert(collection_button_children[i].end(), matchingDiffs.begin(),
-                                                             matchingDiffs.end());
-                }
+            if(songButtonChildren.size() == matching_diffs.size()) {
+                // all diffs match: add the set button (user added all diffs of beatmap into collection)
+                folder.push_back(song_button);
+            } else {
+                // only add matched diff buttons
+                folder.insert(folder.end(), matching_diffs.begin(), matching_diffs.end());
             }
         }
-    }
 
-    for(int i = 0; i < collections.size(); i++) {
-        if(!collection_button_children[i].empty()) {
-            OsuUISongBrowserCollectionButton *collectionButton = new OsuUISongBrowserCollectionButton(
-                m_osu, this, m_songBrowser, m_contextMenu, 250, 250 + m_beatmaps.size() * 50, 200, 50, "",
-                collections[i].name, collection_button_children[i]);
-            m_collectionButtons.push_back(collectionButton);
+        if(!folder.empty()) {
+            UString uname = collection->name.c_str();
+            m_collectionButtons.push_back(
+                new OsuUISongBrowserCollectionButton(m_osu, this, m_songBrowser, m_contextMenu, 250,
+                                                     250 + m_beatmaps.size() * 50, 200, 50, "", uname, folder));
         }
     }
 

+ 1 - 0
src/App/Osu/OsuSongBrowser.h

@@ -339,6 +339,7 @@ class OsuSongBrowser : public OsuScreenBackable {
     std::vector<OsuUISongBrowserCollectionButton *> m_dateaddedCollectionButtons;
     std::vector<OsuUISongBrowserCollectionButton *> m_lengthCollectionButtons;
     std::vector<OsuUISongBrowserCollectionButton *> m_titleCollectionButtons;
+    std::unordered_map<MD5Hash, OsuUISongBrowserSongButton *> hashToSongButton;
     bool m_bBeatmapRefreshScheduled;
     UString m_sLastOsuFolder;
 

+ 49 - 86
src/App/Osu/OsuUISongBrowserCollectionButton.cpp

@@ -7,6 +7,7 @@
 
 #include "OsuUISongBrowserCollectionButton.h"
 
+#include "Collections.h"
 #include "ConVar.h"
 #include "Engine.h"
 #include "Keyboard.h"
@@ -90,25 +91,12 @@ void OsuUISongBrowserCollectionButton::onRightMouseUpInside() { triggerContextMe
 void OsuUISongBrowserCollectionButton::triggerContextMenu(Vector2 pos) {
     if(m_osu->getSongBrowser()->getGroupingMode() != OsuSongBrowser::GROUP::GROUP_COLLECTIONS) return;
 
-    bool isLegacyCollection = false;
-    {
-        const std::vector<OsuDatabase::Collection> &collections =
-            m_osu->getSongBrowser()->getDatabase()->getCollections();
-        for(size_t i = 0; i < collections.size(); i++) {
-            if(collections[i].name == m_sCollectionName) {
-                isLegacyCollection = collections[i].isLegacyCollection;
-
-                break;
-            }
-        }
-    }
-
     if(m_contextMenu != NULL) {
         m_contextMenu->setPos(pos);
         m_contextMenu->setRelPos(pos);
         m_contextMenu->begin(0, true);
         {
-            CBaseUIButton *renameButton = m_contextMenu->addButton("[...]      Rename Collection", 1);
+            m_contextMenu->addButton("[...]      Rename Collection", 1);
 
             CBaseUIButton *spacer = m_contextMenu->addButton("---");
             spacer->setTextLeft(false);
@@ -116,15 +104,7 @@ void OsuUISongBrowserCollectionButton::triggerContextMenu(Vector2 pos) {
             spacer->setTextColor(0xff888888);
             spacer->setTextDarkColor(0xff000000);
 
-            CBaseUIButton *deleteButton = m_contextMenu->addButton("[-]         Delete Collection", 2);
-
-            if(isLegacyCollection) {
-                renameButton->setTextColor(0xff888888);
-                renameButton->setTextDarkColor(0xff000000);
-
-                deleteButton->setTextColor(0xff888888);
-                deleteButton->setTextDarkColor(0xff000000);
-            }
+            m_contextMenu->addButton("[-]         Delete Collection", 2);
         }
         m_contextMenu->end(false, false);
         m_contextMenu->setClickCallback(
@@ -135,88 +115,71 @@ void OsuUISongBrowserCollectionButton::triggerContextMenu(Vector2 pos) {
 }
 
 void OsuUISongBrowserCollectionButton::onContextMenu(UString text, int id) {
-    bool isLegacyCollection = false;
-    {
-        const std::vector<OsuDatabase::Collection> &collections =
-            m_osu->getSongBrowser()->getDatabase()->getCollections();
-        for(size_t i = 0; i < collections.size(); i++) {
-            if(collections[i].name == m_sCollectionName) {
-                isLegacyCollection = collections[i].isLegacyCollection;
+    if(id == 1) {
+        m_contextMenu->begin(0, true);
+        {
+            CBaseUIButton *label = m_contextMenu->addButton("Enter Collection Name:");
+            label->setTextLeft(false);
+            label->setEnabled(false);
 
-                break;
-            }
-        }
-    }
+            CBaseUIButton *spacer = m_contextMenu->addButton("---");
+            spacer->setTextLeft(false);
+            spacer->setEnabled(false);
+            spacer->setTextColor(0xff888888);
+            spacer->setTextDarkColor(0xff000000);
 
-    if(id == 1) {
-        if(!isLegacyCollection) {
+            m_contextMenu->addTextbox(m_sCollectionName, id)->setCursorPosRight();
+
+            spacer = m_contextMenu->addButton("---");
+            spacer->setTextLeft(false);
+            spacer->setEnabled(false);
+            spacer->setTextColor(0xff888888);
+            spacer->setTextDarkColor(0xff000000);
+
+            label = m_contextMenu->addButton("(Press ENTER to confirm.)", id);
+            label->setTextLeft(false);
+            label->setTextColor(0xff555555);
+            label->setTextDarkColor(0xff000000);
+        }
+        m_contextMenu->end(false, false);
+        m_contextMenu->setClickCallback(
+            fastdelegate::MakeDelegate(this, &OsuUISongBrowserCollectionButton::onRenameCollectionConfirmed));
+        OsuUIContextMenu::clampToRightScreenEdge(m_contextMenu);
+        OsuUIContextMenu::clampToBottomScreenEdge(m_contextMenu);
+    } else if(id == 2) {
+        if(engine->getKeyboard()->isShiftDown())
+            onDeleteCollectionConfirmed(text, id);
+        else {
             m_contextMenu->begin(0, true);
             {
-                CBaseUIButton *label = m_contextMenu->addButton("Enter Collection Name:");
-                label->setTextLeft(false);
-                label->setEnabled(false);
-
+                m_contextMenu->addButton("Really delete collection?")->setEnabled(false);
                 CBaseUIButton *spacer = m_contextMenu->addButton("---");
                 spacer->setTextLeft(false);
                 spacer->setEnabled(false);
                 spacer->setTextColor(0xff888888);
                 spacer->setTextDarkColor(0xff000000);
-
-                m_contextMenu->addTextbox(m_sCollectionName, id)->setCursorPosRight();
-
-                spacer = m_contextMenu->addButton("---");
-                spacer->setTextLeft(false);
-                spacer->setEnabled(false);
-                spacer->setTextColor(0xff888888);
-                spacer->setTextDarkColor(0xff000000);
-
-                label = m_contextMenu->addButton("(Press ENTER to confirm.)", id);
-                label->setTextLeft(false);
-                label->setTextColor(0xff555555);
-                label->setTextDarkColor(0xff000000);
+                m_contextMenu->addButton("Yes", 2)->setTextLeft(false);
+                m_contextMenu->addButton("No")->setTextLeft(false);
             }
             m_contextMenu->end(false, false);
             m_contextMenu->setClickCallback(
-                fastdelegate::MakeDelegate(this, &OsuUISongBrowserCollectionButton::onRenameCollectionConfirmed));
+                fastdelegate::MakeDelegate(this, &OsuUISongBrowserCollectionButton::onDeleteCollectionConfirmed));
             OsuUIContextMenu::clampToRightScreenEdge(m_contextMenu);
             OsuUIContextMenu::clampToBottomScreenEdge(m_contextMenu);
-        } else
-            m_osu->getNotificationOverlay()->addNotification("Can't rename collections loaded from osu!", 0xffffff00);
-    } else if(id == 2) {
-        if(!isLegacyCollection) {
-            if(engine->getKeyboard()->isShiftDown())
-                onDeleteCollectionConfirmed(text, id);
-            else {
-                m_contextMenu->begin(0, true);
-                {
-                    m_contextMenu->addButton("Really delete collection?")->setEnabled(false);
-                    CBaseUIButton *spacer = m_contextMenu->addButton("---");
-                    spacer->setTextLeft(false);
-                    spacer->setEnabled(false);
-                    spacer->setTextColor(0xff888888);
-                    spacer->setTextDarkColor(0xff000000);
-                    m_contextMenu->addButton("Yes", 2)->setTextLeft(false);
-                    m_contextMenu->addButton("No")->setTextLeft(false);
-                }
-                m_contextMenu->end(false, false);
-                m_contextMenu->setClickCallback(
-                    fastdelegate::MakeDelegate(this, &OsuUISongBrowserCollectionButton::onDeleteCollectionConfirmed));
-                OsuUIContextMenu::clampToRightScreenEdge(m_contextMenu);
-                OsuUIContextMenu::clampToBottomScreenEdge(m_contextMenu);
-            }
-        } else
-            m_osu->getNotificationOverlay()->addNotification("Can't delete collections loaded from osu!", 0xffffff00);
+        }
     }
 }
 
 void OsuUISongBrowserCollectionButton::onRenameCollectionConfirmed(UString text, int id) {
     if(text.length() > 0) {
-        if(m_osu->getSongBrowser()->getDatabase()->renameCollection(m_sCollectionName, text)) {
-            m_sCollectionName = text;
-
-            // (trigger re-sorting of collection buttons)
-            m_osu->getSongBrowser()->onCollectionButtonContextMenu(this, m_sCollectionName, 3);
-        }
+        std::string old_name = m_sCollectionName.toUtf8();
+        std::string new_name = text.toUtf8();
+        auto collection = get_or_create_collection(old_name);
+        collection->rename_to(new_name);
+        save_collections();
+
+        // (trigger re-sorting of collection buttons)
+        m_osu->getSongBrowser()->onCollectionButtonContextMenu(this, m_sCollectionName, 3);
     }
 }
 

+ 5 - 1
src/App/Osu/OsuUISongBrowserScoreButton.cpp

@@ -12,6 +12,7 @@
 #include "AnimationHandler.h"
 #include "Bancho.h"
 #include "BanchoNetworking.h"
+#include "BanchoUsers.h"
 #include "ConVar.h"
 #include "Console.h"
 #include "Engine.h"
@@ -172,7 +173,7 @@ void OsuUISongBrowserScoreButton::draw(Graphics *g) {
         g->setAlpha(0.75f);
         g->drawString(usernameFont, string);
         g->translate(-0.75f, -0.75f);
-        g->setColor(0xffffffff);
+        g->setColor(is_friend ? 0xffD424B0 : 0xffffffff);
         g->drawString(usernameFont, string);
     }
     g->popTransform();
@@ -628,6 +629,9 @@ void OsuUISongBrowserScoreButton::setScore(const Score &score, const OsuDatabase
     }
     if(score.player_id != 0) {
         m_avatar = new OsuUIAvatar(score.player_id, m_vPos.x, m_vPos.y, m_vSize.y, m_vSize.y);
+
+        auto user = get_user_info(score.player_id);
+        is_friend = user->is_friend();
     }
 
     // display

+ 1 - 0
src/App/Osu/OsuUISongBrowserScoreButton.h

@@ -47,6 +47,7 @@ class OsuUISongBrowserScoreButton : public CBaseUIButton {
     inline UString getDateTime() const { return m_sScoreDateTime; }
     inline int getIndex() const { return m_iScoreIndexNumber; }
 
+    bool is_friend = false;
     OsuUIAvatar *m_avatar = nullptr;
     MD5Hash map_hash;
 

+ 38 - 201
src/App/Osu/OsuUISongBrowserSongButton.cpp

@@ -7,6 +7,7 @@
 
 #include "OsuUISongBrowserSongButton.h"
 
+#include "Collections.h"
 #include "ConVar.h"
 #include "Engine.h"
 #include "Mouse.h"
@@ -305,69 +306,17 @@ void OsuUISongBrowserSongButton::triggerContextMenu(Vector2 pos) {
                     }
                 }
 
-                // check if this entry in the collection is coming from osu! or not
-                // the entry could be either a set button, or an independent diff button
-                bool isLegacyEntry = false;
-                {
-                    const std::vector<OsuDatabase::Collection> &collections =
-                        m_osu->getSongBrowser()->getDatabase()->getCollections();
-                    for(size_t i = 0; i < collections.size(); i++) {
-                        if(collections[i].name == collectionName) {
-                            for(size_t e = 0; e < collections[i].hashes.size(); e++) {
-                                if(m_databaseBeatmap->getDifficulties().size() < 1) {
-                                    // independent diff
-
-                                    if(collections[i].hashes[e].hash == m_databaseBeatmap->getMD5Hash()) {
-                                        isLegacyEntry = collections[i].hashes[e].isLegacyEntry;
-                                        break;
-                                    }
-                                } else {
-                                    // set
-
-                                    const std::vector<OsuDatabaseBeatmap *> &diffs =
-                                        m_databaseBeatmap->getDifficulties();
-
-                                    for(size_t d = 0; d < diffs.size(); d++) {
-                                        if(collections[i].hashes[e].hash == diffs[d]->getMD5Hash()) {
-                                            // one single entry of the set coming from osu! is enough to deny removing
-                                            // the set (as a whole)
-                                            if(collections[i].hashes[e].isLegacyEntry) {
-                                                isLegacyEntry = true;
-                                                break;
-                                            }
-                                        }
-                                    }
-
-                                    if(isLegacyEntry) break;
-                                }
-                            }
-
-                            break;
-                        }
-                    }
-                }
-
                 CBaseUIButton *spacer = m_contextMenu->addButton("---");
                 spacer->setTextLeft(false);
                 spacer->setEnabled(false);
                 spacer->setTextColor(0xff888888);
                 spacer->setTextDarkColor(0xff000000);
 
-                CBaseUIButton *removeDiffButton = NULL;
-                if(m_databaseBeatmap == NULL || m_databaseBeatmap->getDifficulties().size() < 1)
-                    removeDiffButton = m_contextMenu->addButton("[-]          Remove from Collection", 3);
-
-                CBaseUIButton *removeSetButton = m_contextMenu->addButton("[-Set]    Remove from Collection", 4);
-
-                if(isLegacyEntry) {
-                    if(removeDiffButton != NULL) {
-                        removeDiffButton->setTextColor(0xff888888);
-                        removeDiffButton->setTextDarkColor(0xff000000);
-                    }
-
-                    removeSetButton->setTextColor(0xff888888);
-                    removeSetButton->setTextDarkColor(0xff000000);
+                if(m_databaseBeatmap == NULL || m_databaseBeatmap->getDifficulties().size() < 1) {
+                    m_contextMenu->addButton("[-]          Remove from Collection", 3);
                 }
+
+                m_contextMenu->addButton("[-Set]    Remove from Collection", 4);
             }
         }
         m_contextMenu->end(false, false);
@@ -379,98 +328,46 @@ void OsuUISongBrowserSongButton::triggerContextMenu(Vector2 pos) {
 
 void OsuUISongBrowserSongButton::onContextMenu(UString text, int id) {
     if(id == 1 || id == 2) {
+        // 1 = add map to collection
+        // 2 = add set to collection
         m_contextMenu->begin(0, true);
         {
-            const std::vector<OsuDatabase::Collection> &collections =
-                m_osu->getSongBrowser()->getDatabase()->getCollections();
-
             m_contextMenu->addButton("[+]   Create new Collection?", -id * 2);
 
-            if(collections.size() > 0) {
-                CBaseUIButton *spacer = m_contextMenu->addButton("---");
-                spacer->setTextLeft(false);
-                spacer->setEnabled(false);
-                spacer->setTextColor(0xff888888);
-                spacer->setTextDarkColor(0xff000000);
-
-                for(size_t i = 0; i < collections.size(); i++) {
-                    bool isDiffAndAlreadyContained = false;
-                    if(m_databaseBeatmap != NULL && m_databaseBeatmap->getDifficulties().size() < 1) {
-                        for(size_t h = 0; h < collections[i].hashes.size(); h++) {
-                            if(collections[i].hashes[h].hash == m_databaseBeatmap->getMD5Hash()) {
-                                isDiffAndAlreadyContained = true;
-
-                                // edge case: allow adding the set of this diff if it does not represent the entire set
-                                // (and the set is not yet added completely)
-                                if(id == 2) {
-                                    const OsuUISongBrowserSongDifficultyButton *diffButtonPointer =
-                                        dynamic_cast<OsuUISongBrowserSongDifficultyButton *>(this);
-                                    if(diffButtonPointer != NULL && diffButtonPointer->getParentSongButton() != NULL &&
-                                       diffButtonPointer->getParentSongButton()->getDatabaseBeatmap() != NULL) {
-                                        const OsuDatabaseBeatmap *setContainer =
-                                            diffButtonPointer->getParentSongButton()->getDatabaseBeatmap();
-
-                                        if(setContainer->getDifficulties().size() > 1) {
-                                            const std::vector<OsuDatabaseBeatmap *> &diffs =
-                                                setContainer->getDifficulties();
-
-                                            bool isSetAlreadyContained = true;
-                                            for(size_t s = 0; s < diffs.size(); s++) {
-                                                bool isDiffAlreadyContained = false;
-                                                for(size_t h2 = 0; h2 < collections[i].hashes.size(); h2++) {
-                                                    if(diffs[s]->getMD5Hash() == collections[i].hashes[h2].hash) {
-                                                        isDiffAlreadyContained = true;
-                                                        break;
-                                                    }
-                                                }
-
-                                                if(!isDiffAlreadyContained) {
-                                                    isSetAlreadyContained = false;
-                                                    break;
-                                                }
-                                            }
-
-                                            if(!isSetAlreadyContained) isDiffAndAlreadyContained = false;
-                                        }
-                                    }
-                                }
-
-                                break;
-                            }
-                        }
-                    }
-
-                    bool isContainerAndSetAlreadyContained = false;
-                    if(m_databaseBeatmap != NULL && m_databaseBeatmap->getDifficulties().size() > 0) {
-                        const std::vector<OsuDatabaseBeatmap *> &diffs = m_databaseBeatmap->getDifficulties();
-
-                        bool foundAllDiffs = true;
-                        for(size_t d = 0; d < diffs.size(); d++) {
-                            const std::vector<OsuDatabaseBeatmap *> &diffs = m_databaseBeatmap->getDifficulties();
-
-                            bool foundDiff = false;
-                            for(size_t h = 0; h < collections[i].hashes.size(); h++) {
-                                if(collections[i].hashes[h].hash == diffs[d]->getMD5Hash()) {
-                                    foundDiff = true;
+            for(auto collection : collections) {
+                if(!collection->maps.empty()) {
+                    CBaseUIButton *spacer = m_contextMenu->addButton("---");
+                    spacer->setTextLeft(false);
+                    spacer->setEnabled(false);
+                    spacer->setTextColor(0xff888888);
+                    spacer->setTextDarkColor(0xff000000);
+                    break;
+                }
+            }
 
-                                    break;
-                                }
-                            }
+            auto map_hash = m_databaseBeatmap->getMD5Hash();
+            for(auto collection : collections) {
+                if(collection->maps.empty()) continue;
 
-                            foundAllDiffs = foundDiff;
-                            if(!foundAllDiffs) break;
-                        }
+                bool can_add_to_collection = true;
 
-                        isContainerAndSetAlreadyContained = foundAllDiffs;
+                if(id == 1) {
+                    auto it = std::find(collection->maps.begin(), collection->maps.end(), map_hash);
+                    if(it != collection->maps.end()) {
+                        // Map already is present in the collection
+                        can_add_to_collection = false;
                     }
+                }
 
-                    CBaseUIButton *collectionButton = m_contextMenu->addButton(collections[i].name, id);
+                if(id == 2) {
+                    // XXX: Don't mark as valid if the set is fully present in the collection
+                }
 
-                    if(isDiffAndAlreadyContained || isContainerAndSetAlreadyContained) {
-                        collectionButton->setEnabled(false);
-                        collectionButton->setTextColor(0xff555555);
-                        collectionButton->setTextDarkColor(0xff000000);
-                    }
+                auto collectionButton = m_contextMenu->addButton(UString(collection->name.c_str()), id);
+                if(!can_add_to_collection) {
+                    collectionButton->setEnabled(false);
+                    collectionButton->setTextColor(0xff555555);
+                    collectionButton->setTextDarkColor(0xff000000);
                 }
             }
         }
@@ -480,69 +377,9 @@ void OsuUISongBrowserSongButton::onContextMenu(UString text, int id) {
         OsuUIContextMenu::clampToRightScreenEdge(m_contextMenu);
         OsuUIContextMenu::clampToBottomScreenEdge(m_contextMenu);
     } else if(id == 3 || id == 4) {
-        // get the collection name for this diff/set
-        UString collectionName;
-        {
-            const std::vector<OsuUISongBrowserCollectionButton *> &collectionButtons =
-                m_osu->getSongBrowser()->getCollectionButtons();
-            for(size_t i = 0; i < collectionButtons.size(); i++) {
-                if(collectionButtons[i]->isSelected()) {
-                    collectionName = collectionButtons[i]->getCollectionName();
-                    break;
-                }
-            }
-        }
-
-        // check if this entry in the collection is coming from osu! or not
-        // the entry could be either a set button, or an independent diff button
-        bool isLegacyEntry = false;
-        {
-            const std::vector<OsuDatabase::Collection> &collections =
-                m_osu->getSongBrowser()->getDatabase()->getCollections();
-            for(size_t i = 0; i < collections.size(); i++) {
-                if(collections[i].name == collectionName) {
-                    for(size_t e = 0; e < collections[i].hashes.size(); e++) {
-                        if(m_databaseBeatmap->getDifficulties().size() < 1) {
-                            // independent diff
-
-                            if(collections[i].hashes[e].hash == m_databaseBeatmap->getMD5Hash()) {
-                                isLegacyEntry = collections[i].hashes[e].isLegacyEntry;
-                                break;
-                            }
-                        } else {
-                            // set
-
-                            const std::vector<OsuDatabaseBeatmap *> &diffs = m_databaseBeatmap->getDifficulties();
-
-                            for(size_t d = 0; d < diffs.size(); d++) {
-                                if(collections[i].hashes[e].hash == diffs[d]->getMD5Hash()) {
-                                    // one single entry of the set coming from osu! is enough to deny removing the set
-                                    // (as a whole)
-                                    if(collections[i].hashes[e].isLegacyEntry) {
-                                        isLegacyEntry = true;
-                                        break;
-                                    }
-                                }
-                            }
-
-                            if(isLegacyEntry) break;
-                        }
-                    }
-
-                    break;
-                }
-            }
-        }
-
-        if(isLegacyEntry) {
-            if(id == 3)
-                m_osu->getNotificationOverlay()->addNotification("Can't remove collection entry loaded from osu!",
-                                                                 0xffffff00);
-            else if(id == 4)
-                m_osu->getNotificationOverlay()->addNotification("Can't remove collection set loaded from osu!",
-                                                                 0xffffff00);
-        } else
-            m_osu->getSongBrowser()->onSongButtonContextMenu(this, text, id);
+        // 3 = remove map from collection
+        // 4 = remove set from collection
+        m_osu->getSongBrowser()->onSongButtonContextMenu(this, text, id);
     }
 }
 

+ 6 - 4
src/App/Osu/OsuUIUserContextMenu.cpp

@@ -63,7 +63,7 @@ void OsuUIUserContextMenuScreen::open(uint32_t user_id) {
             // menu->addButton("Invite to game", INVITE_TO_GAME);
         }
 
-        if(user_info->is_friend) {
+        if(user_info->is_friend()) {
             menu->addButton("Revoke friendship", UA_REMOVE_FRIEND);
         } else {
             menu->addButton("Add as friend", UA_ADD_FRIEND);
@@ -115,15 +115,17 @@ void OsuUIUserContextMenuScreen::on_action(UString text, int user_action) {
         packet.id = FRIEND_ADD;
         write_int32(&packet, m_user_id);
         send_packet(packet);
-
-        user_info->is_friend = true;
+        friends.push_back(m_user_id);
     } else if(user_action == UA_REMOVE_FRIEND) {
         Packet packet;
         packet.id = FRIEND_REMOVE;
         write_int32(&packet, m_user_id);
         send_packet(packet);
 
-        user_info->is_friend = false;
+        auto it = std::find(friends.begin(), friends.end(), m_user_id);
+        if(it != friends.end()) {
+            friends.erase(it);
+        }
     }
 
     menu->setVisible(false);

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

@@ -261,7 +261,7 @@ bool OsuVolumeOverlay::canChangeVolume() {
 }
 
 void OsuVolumeOverlay::gainFocus() {
-    if(engine->getSound()->isWASAPI() && convar->getConVarByName("win_snd_wasapi_exclusive")->getBool()) {
+    if(engine->getSound()->isWASAPI()) {
         // NOTE: wasapi exclusive mode controls the system volume, so don't bother
         return;
     }
@@ -271,7 +271,7 @@ void OsuVolumeOverlay::gainFocus() {
 }
 
 void OsuVolumeOverlay::loseFocus() {
-    if(engine->getSound()->isWASAPI() && convar->getConVarByName("win_snd_wasapi_exclusive")->getBool()) {
+    if(engine->getSound()->isWASAPI()) {
         // NOTE: wasapi exclusive mode controls the system volume, so don't bother
         return;
     }

+ 2 - 10
src/Engine/SoundEngine.cpp

@@ -59,8 +59,6 @@ ConVar win_snd_wasapi_buffer_size(
 ConVar win_snd_wasapi_period_size(
     "win_snd_wasapi_period_size", 0.0f, FCVAR_NONE,
     "interval between OutputWasapiProc calls in seconds (e.g. 0.016 = 16 ms) (0 = use default)");
-ConVar win_snd_wasapi_exclusive("win_snd_wasapi_exclusive", false, FCVAR_NONE,
-                                "whether to use exclusive device mode to further reduce latency");
 
 ConVar asio_buffer_size("asio_buffer_size", -1, FCVAR_NONE,
                         "buffer size in samples (usually 44100 samples per second)");
@@ -239,10 +237,6 @@ void SoundEngine::updateOutputDevices(bool printInfo) {
         }
 
         soundDevice.driver = OutputDriver::BASS;
-        if(env->getOS() == Environment::OS::OS_WINDOWS && soundDevice.name != UString("No sound")) {
-            soundDevice.name.append(" [DirectSound]");
-        }
-
         m_outputDevices.push_back(soundDevice);
 
         debugLog("DSOUND: Device %i = \"%s\", enabled = %i, default = %i\n", d, deviceInfo.name, (int)isEnabled,
@@ -486,11 +480,9 @@ bool SoundEngine::initializeOutputDevice(OUTPUT_DEVICE device) {
 
         // BASS_WASAPI_RAW ignores sound "enhancements" that some sound cards offer (adds latency)
         // BASS_MIXER_NONSTOP prevents some sound cards from going to sleep when there is no output
-        auto flags = BASS_WASAPI_RAW | BASS_MIXER_NONSTOP;
-        if(win_snd_wasapi_exclusive.getBool()) flags |= BASS_WASAPI_EXCLUSIVE;
+        auto flags = BASS_WASAPI_RAW | BASS_MIXER_NONSTOP | BASS_WASAPI_EXCLUSIVE;
 
-        debugLog("WASAPI Exclusive Mode = %i, bufferSize = %f, updatePeriod = %f\n",
-                 (int)win_snd_wasapi_exclusive.getBool(), bufferSize, updatePeriod);
+        debugLog("WASAPI bufferSize = %f, updatePeriod = %f\n", bufferSize, updatePeriod);
         if(!BASS_WASAPI_Init(device.id, 0, 0, flags, bufferSize, updatePeriod, OutputWasapiProc, NULL)) {
             const int errorCode = BASS_ErrorGetCode();
             if(errorCode == BASS_ERROR_WASAPI_BUFFER) {

+ 3 - 3
src/Engine/SoundEngine.h

@@ -13,9 +13,9 @@
 
 enum class OutputDriver {
     NONE,
-    BASS,
-    BASS_WASAPI,
-    BASS_ASIO,
+    BASS,         // directsound/wasapi non-exclusive mode/alsa
+    BASS_WASAPI,  // exclusive mode
+    BASS_ASIO,    // exclusive move
 };
 
 struct OUTPUT_DEVICE {