Browse Source

Handle map links

kiwec 2 months ago
parent
commit
3483c45088

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

@@ -31,6 +31,7 @@ Changelog::Changelog() : ScreenBackable() {
         UString::format("%.2f (%s, %s)", convar->getConVarByName("osu_version")->getFloat(), __DATE__, __TIME__);
     latest.changes.push_back("- Chat: added support for /me command");
     latest.changes.push_back("- Chat: added support for links");
+    latest.changes.push_back("- Chat: added support for map links (auto-downloads)");
     changelogs.push_back(latest);
 
     CHANGELOG v35_05;

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

@@ -1,6 +1,12 @@
 #include "ChatLink.h"
 
+#include <codecvt>
+#include <regex>
+
+#include "Bancho.h"
+#include "MainMenu.h"
 #include "Osu.h"
+#include "SongBrowser/SongBrowser.h"
 #include "TooltipOverlay.h"
 
 ChatLink::ChatLink(float xPos, float yPos, float xSize, float ySize, UString link, UString label)
@@ -26,7 +32,47 @@ void ChatLink::mouse_update(bool *propagate_clicks) {
 }
 
 void ChatLink::onMouseUpInside() {
-    // XXX: Handle map links, on click auto-download, add to song browser and select
-    // XXX: Handle lobby invite links, on click join the lobby (even if passworded)
+    // TODO: Handle lobby invite links, on click join the lobby (even if passworded)
+
+    // This lazy escaping is only good for endpoint URLs, not anything more serious
+    UString escaped_endpoint;
+    for(int i = 0; i < bancho.endpoint.length(); i++) {
+        if(bancho.endpoint[i] == L'.') {
+            escaped_endpoint.append("\\.");
+        } else {
+            escaped_endpoint.append(bancho.endpoint[i]);
+        }
+    }
+
+    // https:\/\/(akatsuki.gg|osu.ppy.sh)\/b(eatmapsets\/\d+#(osu)?)?\/(\d+)
+    UString map_pattern = "https://(osu\\.";
+    map_pattern.append(escaped_endpoint);
+    map_pattern.append("|osu.ppy.sh)/b(eatmapsets/(\\d+)#(osu)?)?/(\\d+)");
+    std::wregex map_regex(map_pattern.wc_str());
+
+    std::wstring link_str = m_link.wc_str();
+    std::wsmatch match;
+    if(std::regex_search(link_str, match, map_regex)) {
+        i32 map_id = std::stoi(std::wstring_convert<std::codecvt_utf8<wchar_t>>().to_bytes(match.str(5)));
+        i32 set_id = 0;
+        if(match[3].matched) {
+            // Set ID doesn't match if the URL only contains the map ID
+            set_id = std::stoi(std::wstring_convert<std::codecvt_utf8<wchar_t>>().to_bytes(match.str(3)));
+        }
+
+        if(osu->getSongBrowser()->isVisible()) {
+            osu->getSongBrowser()->map_autodl = map_id;
+            osu->getSongBrowser()->set_autodl = set_id;
+        } else if(osu->getMainMenu()->isVisible()) {
+            osu->toggleSongBrowser();
+            osu->getSongBrowser()->map_autodl = map_id;
+            osu->getSongBrowser()->set_autodl = set_id;
+        } else {
+            env->openURLInDefaultBrowser(m_link);
+        }
+
+        return;
+    }
+
     env->openURLInDefaultBrowser(m_link);
 }

+ 17 - 0
src/App/Osu/Database.cpp

@@ -784,6 +784,23 @@ DatabaseBeatmap *Database::getBeatmapDifficulty(const MD5Hash &md5hash) {
     return NULL;
 }
 
+DatabaseBeatmap *Database::getBeatmapDifficulty(i32 map_id) {
+    if(!isFinished()) return NULL;
+
+    for(size_t i = 0; i < m_databaseBeatmaps.size(); i++) {
+        DatabaseBeatmap *beatmap = m_databaseBeatmaps[i];
+        const std::vector<DatabaseBeatmap *> &diffs = beatmap->getDifficulties();
+        for(size_t d = 0; d < diffs.size(); d++) {
+            DatabaseBeatmap *diff = diffs[d];
+            if(diff->getID() == map_id) {
+                return diff;
+            }
+        }
+    }
+
+    return NULL;
+}
+
 std::string Database::parseLegacyCfgBeatmapDirectoryParameter() {
     // get BeatmapDirectory parameter from osu!.<OS_USERNAME>.cfg
     debugLog("Database::parseLegacyCfgBeatmapDirectoryParameter() : username = %s\n", env->getUsername().toUtf8());

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

@@ -88,6 +88,7 @@ class Database {
 
     inline const std::vector<DatabaseBeatmap *> getDatabaseBeatmaps() const { return m_databaseBeatmaps; }
     DatabaseBeatmap *getBeatmapDifficulty(const MD5Hash &md5hash);
+    DatabaseBeatmap *getBeatmapDifficulty(i32 map_id);
 
     inline std::unordered_map<MD5Hash, std::vector<FinishedScore>> *getScores() { return &m_scores; }
     inline const std::vector<SCORE_SORTING_METHOD> &getScoreSortingMethods() const { return m_scoreSortingMethods; }

+ 76 - 4
src/App/Osu/Downloader.cpp

@@ -1,10 +1,10 @@
 #include "Downloader.h"
 
 #include <curl/curl.h>
-#include <mutex>
-#include <thread>
 
+#include <mutex>
 #include <sstream>
+#include <thread>
 
 #include "Bancho.h"
 #include "BanchoNetworking.h"
@@ -40,7 +40,7 @@ void abort_downloads() {
 }
 
 int update_download_progress(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal,
-                              curl_off_t ulnow) {
+                             curl_off_t ulnow) {
     (void)ultotal;
     (void)ulnow;
 
@@ -52,7 +52,7 @@ int update_download_progress(void* clientp, curl_off_t dltotal, curl_off_t dlnow
         result->progress = (float)dlnow / (float)dltotal;
     }
 
-    return CURL_PROGRESSFUNC_CONTINUE;
+    return 0;
 }
 
 void* do_downloads(void* arg) {
@@ -349,6 +349,78 @@ DatabaseBeatmap* download_beatmap(i32 beatmap_id, MD5Hash beatmap_md5, float* pr
     return beatmap;
 }
 
+DatabaseBeatmap* download_beatmap(i32 beatmap_id, i32 beatmapset_id, float* progress) {
+    static i32 queried_map_id = 0;
+
+    auto beatmap = osu->getSongBrowser()->getDatabase()->getBeatmapDifficulty(beatmap_id);
+    if(beatmap != NULL) {
+        *progress = 1.f;
+        return beatmap;
+    }
+
+    // XXX: Currently, we do not try to find the difficulty from unloaded database, or from neosu downloaded maps
+    auto it = beatmap_to_beatmapset.find(beatmap_id);
+    if(it == beatmap_to_beatmapset.end()) {
+        if(queried_map_id == beatmap_id) {
+            // We already queried for the beatmapset ID, and are waiting for the response
+            *progress = 0.f;
+            return NULL;
+        }
+
+        // We already have the set ID, skip the API request
+        if(beatmapset_id != 0) {
+            beatmap_to_beatmapset[beatmap_id] = beatmapset_id;
+            *progress = 0.f;
+            return NULL;
+        }
+
+        APIRequest request;
+        request.type = GET_BEATMAPSET_INFO;
+        request.path = UString::format("/web/osu-search-set.php?b=%d&u=%s&h=%s", beatmap_id, bancho.username.toUtf8(),
+                                       bancho.pw_md5.toUtf8());
+        request.extra_int = beatmap_id;
+        send_api_request(request);
+
+        queried_map_id = beatmap_id;
+
+        *progress = 0.f;
+        return NULL;
+    }
+
+    i32 set_id = it->second;
+    if(set_id == 0) {
+        // Already failed to load the beatmap
+        *progress = -1.f;
+        return NULL;
+    }
+
+    download_beatmapset(set_id, progress);
+    if(*progress == -1.f) {
+        // Download failed, don't retry
+        beatmap_to_beatmapset[beatmap_id] = 0;
+        return NULL;
+    }
+
+    // Download not finished
+    if(*progress != 1.f) return NULL;
+
+    auto mapset_path = UString::format(MCENGINE_DATA_DIR "maps/%d/", set_id);
+    // XXX: Make a permanent database for auto-downloaded songs, so we can load them like osu!.db's
+    osu->m_songBrowser2->getDatabase()->addBeatmap(mapset_path.toUtf8());
+    osu->m_songBrowser2->updateSongButtonSorting();
+    debugLog("Finished loading beatmapset %d.\n", set_id);
+
+    beatmap = osu->getSongBrowser()->getDatabase()->getBeatmapDifficulty(beatmap_id);
+    if(beatmap == NULL) {
+        beatmap_to_beatmapset[beatmap_id] = 0;
+        *progress = -1.f;
+        return NULL;
+    }
+
+    *progress = 1.f;
+    return beatmap;
+}
+
 void process_beatmapset_info_response(Packet packet) {
     i32 map_id = packet.extra_int;
     if(packet.size == 0) {

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

@@ -18,4 +18,5 @@ void download_beatmapset(u32 set_id, float *progress);
 // Downloads given beatmap (unless it already exists)
 // When download/extraction fails, `progress` is -1
 DatabaseBeatmap *download_beatmap(i32 beatmap_id, MD5Hash beatmap_md5, float *progress);
+DatabaseBeatmap *download_beatmap(i32 beatmap_id, i32 beatmapset_id, float *progress);
 void process_beatmapset_info_response(Packet packet);

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

@@ -275,7 +275,7 @@ class Osu : public App, public MouseListener {
     bool holding_slider = false;
 
     // mods
-    u32 previous_mod_flags = 0;  // mod flags before spectating/multiplaying/etc
+    u32 previous_mod_flags = 0;   // mod flags before spectating/multiplaying/etc
     bool m_bModAutoTemp = false;  // when ctrl+clicking a map, the auto mod should disable itself after the map finishes
     bool m_bModAuto = false;
     bool m_bModAutopilot = false;

+ 35 - 18
src/App/Osu/SongBrowser/SongBrowser.cpp

@@ -22,6 +22,7 @@
 #include "ConVar.h"
 #include "Database.h"
 #include "DatabaseBeatmap.h"
+#include "Downloader.h"
 #include "Engine.h"
 #include "HUD.h"
 #include "Icons.h"
@@ -903,13 +904,10 @@ void SongBrowser::mouse_update(bool *propagate_clicks) {
             db_diff->m_calculate_full_pp = std::future<pp_info>();
 
             m_selectedBeatmap->m_aimStrains = std::vector<f64>(
-                db_diff->m_pp_info.aim_strains,
-                &db_diff->m_pp_info.aim_strains[db_diff->m_pp_info.aim_strains_len]
-            );
-            m_selectedBeatmap->m_speedStrains = std::vector<f64>(
-                db_diff->m_pp_info.speed_strains,
-                &db_diff->m_pp_info.speed_strains[db_diff->m_pp_info.speed_strains_len]
-            );
+                db_diff->m_pp_info.aim_strains, &db_diff->m_pp_info.aim_strains[db_diff->m_pp_info.aim_strains_len]);
+            m_selectedBeatmap->m_speedStrains =
+                std::vector<f64>(db_diff->m_pp_info.speed_strains,
+                                 &db_diff->m_pp_info.speed_strains[db_diff->m_pp_info.speed_strains_len]);
 
             // Free the strains, which we already copied into vectors
             free_pp_info(db_diff->m_pp_info);
@@ -928,6 +926,26 @@ void SongBrowser::mouse_update(bool *propagate_clicks) {
         return;
     }
 
+    // auto-download
+    if(map_autodl) {
+        float progress = -1.f;
+        auto beatmap = download_beatmap(map_autodl, set_autodl, &progress);
+        if(progress == -1.f) {
+            auto error_str = UString::format("Failed to download Beatmap #%d :(", map_autodl);
+            osu->getNotificationOverlay()->addNotification(error_str);
+            map_autodl = 0;
+            set_autodl = 0;
+        } else if(progress < 1.f) {
+            // TODO @kiwec: this notification format is jank & laggy
+            auto text = UString::format("Downloading... %.2f%%", progress * 100.f);
+            osu->getNotificationOverlay()->addNotification(text);
+        } else if(beatmap != NULL) {
+            osu->m_songBrowser2->onDifficultySelected(beatmap, false);
+            map_autodl = 0;
+            set_autodl = 0;
+        }
+    }
+
     // update and focus handling
     m_contextMenu->mouse_update(propagate_clicks);
     m_songBrowser->mouse_update(propagate_clicks);
@@ -2291,7 +2309,7 @@ void SongBrowser::updateLayout() {
     m_fSongSelectTopScale = Osu::getImageScaleToFitResolution(osu->getSkin()->getSongSelectTop(), osu->getScreenSize());
     const float songSelectTopHeightScaled =
         max(osu->getSkin()->getSongSelectTop()->getHeight() * m_fSongSelectTopScale,
-                 m_songInfo->getMinimumHeight() * 1.5f + margin);  // NOTE: the height is a heuristic here more or less
+            m_songInfo->getMinimumHeight() * 1.5f + margin);  // NOTE: the height is a heuristic here more or less
     m_fSongSelectTopScale =
         max(m_fSongSelectTopScale, songSelectTopHeightScaled / osu->getSkin()->getSongSelectTop()->getHeight());
     m_fSongSelectTopScale *=
@@ -2300,12 +2318,12 @@ void SongBrowser::updateLayout() {
     // topbar left (NOTE: the right side of the max() width is commented to keep the scorebrowser width consistent,
     // and because it's not really needed anyway)
     m_topbarLeft->setSize(max(osu->getSkin()->getSongSelectTop()->getWidth() * m_fSongSelectTopScale *
-                                           osu_songbrowser_topbar_left_width_percent.getFloat() +
-                                       margin,
-                                   /*m_songInfo->getMinimumWidth() + margin*/ 0.0f),
+                                      osu_songbrowser_topbar_left_width_percent.getFloat() +
+                                  margin,
+                              /*m_songInfo->getMinimumWidth() + margin*/ 0.0f),
                           max(osu->getSkin()->getSongSelectTop()->getHeight() * m_fSongSelectTopScale *
-                                       osu_songbrowser_topbar_left_percent.getFloat(),
-                                   m_songInfo->getMinimumHeight() + margin));
+                                  osu_songbrowser_topbar_left_percent.getFloat(),
+                              m_songInfo->getMinimumHeight() + margin));
     m_songInfo->setRelPos(margin, margin);
     m_songInfo->setSize(m_topbarLeft->getSize().x - margin,
                         max(m_topbarLeft->getSize().y * 0.75f, m_songInfo->getMinimumHeight() + margin));
@@ -2436,8 +2454,8 @@ void SongBrowser::updateLayout() {
     const int userButtonHeight = m_bottombar->getSize().y * 0.9f;
     m_userButton->setSize(userButtonHeight * 3.5f, userButtonHeight);
     m_userButton->setRelPos(max(m_bottombar->getSize().x / 2 - m_userButton->getSize().x / 2,
-                                     m_bottombarNavButtons[m_bottombarNavButtons.size() - 1]->getRelPos().x +
-                                         m_bottombarNavButtons[m_bottombarNavButtons.size() - 1]->getSize().x + 10),
+                                m_bottombarNavButtons[m_bottombarNavButtons.size() - 1]->getRelPos().x +
+                                    m_bottombarNavButtons[m_bottombarNavButtons.size() - 1]->getSize().x + 10),
                             m_bottombar->getSize().y - m_userButton->getSize().y - 1);
 
     m_bottombar->update_pos();
@@ -2553,9 +2571,8 @@ void SongBrowser::rebuildScoreButtons() {
     std::vector<FinishedScore> scores;
     if(validBeatmap) {
         auto local_scores = (*m_db->getScores())[m_selectedBeatmap->getSelectedDifficulty2()->getMD5Hash()];
-        auto local_best =
-            max_element(local_scores.begin(), local_scores.end(),
-                             [](FinishedScore const &a, FinishedScore const &b) { return a.score < b.score; });
+        auto local_best = max_element(local_scores.begin(), local_scores.end(),
+                                      [](FinishedScore const &a, FinishedScore const &b) { return a.score < b.score; });
 
         if(is_online) {
             auto search = m_db->m_online_scores.find(m_selectedBeatmap->getSelectedDifficulty2()->getMD5Hash());

+ 4 - 0
src/App/Osu/SongBrowser/SongBrowser.h

@@ -349,6 +349,10 @@ class SongBrowser : public ScreenBackable {
     float m_fBackgroundFadeInTime;
     std::vector<DatabaseBeatmap *> m_previousRandomBeatmaps;
 
+    // map auto-download
+    i32 map_autodl = 0;
+    i32 set_autodl = 0;
+
     // search
     UISearchOverlay *m_search;
     UString m_sSearchString;