Browse Source

Import .osz beatmapsets by dropping on window

kiwec 2 months ago
parent
commit
ac69797977

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

@@ -29,13 +29,13 @@ Changelog::Changelog() : ScreenBackable() {
     CHANGELOG latest;
     latest.title =
         UString::format("%.2f (%s, %s)", convar->getConVarByName("osu_version")->getFloat(), __DATE__, __TIME__);
+    latest.changes.push_back("- Added ability to drag-and-drop .osk and .osz files onto neosu");
+    latest.changes.push_back("- Added local skin folder");
     latest.changes.push_back("- Fixed songs failing to restart");
     changelogs.push_back(latest);
 
     CHANGELOG v35_07;
     v35_07.title = "35.07 (2024-06-27)";
-    v35_07.changes.push_back("- Added ability to drag-and-drop skins onto neosu");
-    v35_07.changes.push_back("- Added local skin folder");
     v35_07.changes.push_back("- Added sort_skins_by_name convar");
     v35_07.changes.push_back("- Added setting to prevent servers from replacing the main menu logo");
     v35_07.changes.push_back("- Chat: added missing chat commands");

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

@@ -392,7 +392,8 @@ void Database::update() {
 
             // update progress (another thread checks if progress >= 1.f to know when we're done)
             float progress = (float)m_iCurRawBeatmapLoadIndex / (float)m_iNumBeatmapsToLoad;
-            if(progress >= 1.0f) progress = 0.99f;
+            if(progress == 0.f) progress = 0.01f;
+            if(progress >= 1.f) progress = 0.99f;
             m_fLoadingProgress = progress;
 
             // check if we are finished
@@ -768,7 +769,7 @@ int Database::getLevelForScore(unsigned long long score, int maxLevel) {
 }
 
 DatabaseBeatmap *Database::getBeatmapDifficulty(const MD5Hash &md5hash) {
-    if(!isFinished()) return NULL;
+    if(isLoading()) return NULL;
 
     for(size_t i = 0; i < m_databaseBeatmaps.size(); i++) {
         DatabaseBeatmap *beatmap = m_databaseBeatmaps[i];
@@ -785,7 +786,7 @@ DatabaseBeatmap *Database::getBeatmapDifficulty(const MD5Hash &md5hash) {
 }
 
 DatabaseBeatmap *Database::getBeatmapDifficulty(i32 map_id) {
-    if(!isFinished()) return NULL;
+    if(isLoading()) return NULL;
 
     for(size_t i = 0; i < m_databaseBeatmaps.size(); i++) {
         DatabaseBeatmap *beatmap = m_databaseBeatmaps[i];
@@ -802,7 +803,7 @@ DatabaseBeatmap *Database::getBeatmapDifficulty(i32 map_id) {
 }
 
 DatabaseBeatmap *Database::getBeatmapSet(i32 set_id) {
-    if(!isFinished()) return NULL;
+    if(isLoading()) return NULL;
 
     for(size_t i = 0; i < m_databaseBeatmaps.size(); i++) {
         DatabaseBeatmap *beatmap = m_databaseBeatmaps[i];
@@ -1008,6 +1009,7 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
 
         // update progress (another thread checks if progress >= 1.f to know when we're done)
         float progress = (float)i / (float)m_iNumBeatmapsToLoad;
+        if(progress == 0.f) progress = 0.01f;
         if(progress >= 1.f) progress = 0.99f;
         m_fLoadingProgress = progress;
 

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

@@ -83,6 +83,10 @@ class Database {
     static int getLevelForScore(unsigned long long score, int maxLevel = 120);
 
     inline float getProgress() const { return m_fLoadingProgress.load(); }
+    inline bool isLoading() const {
+        float progress = getProgress();
+        return progress > 0.f && progress < 1.f;
+    }
     inline bool isFinished() const { return (getProgress() >= 1.0f); }
     inline bool foundChanges() const { return m_bFoundChanges; }
 

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

@@ -147,6 +147,7 @@ DatabaseBeatmap::DatabaseBeatmap(std::vector<DatabaseBeatmap *> *difficulties) :
     m_sArtist = (*m_difficulties)[0]->m_sArtist;
     m_sCreator = (*m_difficulties)[0]->m_sCreator;
     m_sBackgroundImageFileName = (*m_difficulties)[0]->m_sBackgroundImageFileName;
+    m_iSetID = (*m_difficulties)[0]->m_iSetID;
 
     // also calculate largest representative values
     m_iLengthMS = 0;

+ 92 - 22
src/App/Osu/Downloader.cpp

@@ -215,49 +215,96 @@ end:
     curl_url_cleanup(urlu);
 }
 
-void download_beatmapset(u32 set_id, float* progress) {
-    // Check if we already have downloaded it
-    auto map_dir = UString::format(MCENGINE_DATA_DIR "maps/%d/", set_id);
-    if(env->directoryExists(map_dir.toUtf8())) {
-        *progress = 1.f;
-        return;
+i32 get_beatmapset_id_from_osu_file(const u8* osu_data, size_t s_osu_data) {
+    i32 set_id = -1;
+    std::string line;
+    for(size_t i = 0; i < s_osu_data; i++) {
+        if(osu_data[i] == '\n') {
+            if(line.find("//") != 0) {
+                debugLog("%s\n", line.c_str());
+                sscanf(line.c_str(), " BeatmapSetID : %i \n", &set_id);
+                if(set_id != -1) return set_id;
+            }
+
+            line = "";
+        } else {
+            line.push_back(osu_data[i]);
+        }
     }
 
-    std::vector<u8> data;
-    auto mirror = convar->getConVarByName("beatmap_mirror")->getString();
-    mirror.append(UString::format("%d", set_id));
-    int response_code = 0;
-    download(mirror.toUtf8(), progress, data, &response_code);
-    if(response_code != 200) return;
+    if(line.find("//") != 0) {
+        debugLog("%s\n", line.c_str());
+        sscanf(line.c_str(), " BeatmapSetID : %i \n", &set_id);
+    }
+
+    return -1;
+}
+
+i32 extract_beatmapset_id(const u8* data, size_t data_s) {
+    i32 set_id = -1;
 
-    // Download succeeded: save map to disk
     mz_zip_archive zip = {0};
     mz_zip_archive_file_stat file_stat;
     mz_uint num_files = 0;
 
-    debugLog("Extracting beatmapset %d (%d bytes)\n", set_id, data.size());
-    if(!mz_zip_reader_init_mem(&zip, data.data(), data.size(), 0)) {
+    debugLog("Reading beatmapset (%d bytes)\n", data_s);
+    if(!mz_zip_reader_init_mem(&zip, data, data_s, 0)) {
         debugLog("Failed to open .osz file\n");
-        *progress = -1.f;
-        return;
+        return -1;
     }
 
     num_files = mz_zip_reader_get_num_files(&zip);
     if(num_files <= 0) {
         debugLog(".osz file is empty!\n");
         mz_zip_reader_end(&zip);
-        *progress = -1.f;
-        return;
+        return -1;
     }
-    if(!env->directoryExists(map_dir.toUtf8())) {
-        env->createDirectory(map_dir.toUtf8());
+    for(mz_uint i = 0; i < num_files; i++) {
+        if(!mz_zip_reader_file_stat(&zip, i, &file_stat)) continue;
+        if(mz_zip_reader_is_file_a_directory(&zip, i)) continue;
+        if(env->getFileExtensionFromFilePath(file_stat.m_filename).compare("osu") != 0) continue;
+
+        size_t s_osu_data = 0;
+        u8* osu_data = (u8*)mz_zip_reader_extract_to_heap(&zip, i, &s_osu_data, 0);
+        set_id = get_beatmapset_id_from_osu_file(osu_data, s_osu_data);
+        mz_free(osu_data);
+        if(set_id != -1) break;
+
+    skip_file:;
+        // When a file can't be extracted we just ignore it (as long as the archive is valid).
+        // We'll check for errors when loading the beatmap.
+    }
+
+    mz_zip_reader_end(&zip);
+    return set_id;
+}
+
+bool extract_beatmapset(const u8* data, size_t data_s, std::string map_dir) {
+    mz_zip_archive zip = {0};
+    mz_zip_archive_file_stat file_stat;
+    mz_uint num_files = 0;
+
+    debugLog("Extracting beatmapset (%d bytes)\n", data_s);
+    if(!mz_zip_reader_init_mem(&zip, data, data_s, 0)) {
+        debugLog("Failed to open .osz file\n");
+        return false;
+    }
+
+    num_files = mz_zip_reader_get_num_files(&zip);
+    if(num_files <= 0) {
+        debugLog(".osz file is empty!\n");
+        mz_zip_reader_end(&zip);
+        return false;
+    }
+    if(!env->directoryExists(map_dir)) {
+        env->createDirectory(map_dir);
     }
     for(mz_uint i = 0; i < num_files; i++) {
         if(!mz_zip_reader_file_stat(&zip, i, &file_stat)) continue;
         if(mz_zip_reader_is_file_a_directory(&zip, i)) continue;
 
         auto folders = UString(file_stat.m_filename).split("/");
-        std::string file_path = map_dir.toUtf8();
+        std::string file_path = map_dir;
         for(auto folder : folders) {
             if(!env->directoryExists(file_path)) {
                 env->createDirectory(file_path);
@@ -281,6 +328,29 @@ void download_beatmapset(u32 set_id, float* progress) {
 
     // Success
     mz_zip_reader_end(&zip);
+    return true;
+}
+
+void download_beatmapset(u32 set_id, float* progress) {
+    // Check if we already have downloaded it
+    auto map_dir = UString::format(MCENGINE_DATA_DIR "maps/%d/", set_id);
+    if(env->directoryExists(map_dir.toUtf8())) {
+        *progress = 1.f;
+        return;
+    }
+
+    std::vector<u8> data;
+    auto mirror = convar->getConVarByName("beatmap_mirror")->getString();
+    mirror.append(UString::format("%d", set_id));
+    int response_code = 0;
+    download(mirror.toUtf8(), progress, data, &response_code);
+    if(response_code != 200) return;
+
+    // Download succeeded: save map to disk
+    if(!extract_beatmapset(data.data(), data.size(), map_dir.toUtf8())) {
+        *progress = -1.f;
+        return;
+    }
 }
 
 std::unordered_map<i32, i32> beatmap_to_beatmapset;

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

@@ -20,3 +20,6 @@ void download_beatmapset(u32 set_id, float *progress);
 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);
+
+i32 extract_beatmapset_id(const u8* data, size_t data_s);
+bool extract_beatmapset(const u8* data, size_t data_s, std::string map_dir);

+ 53 - 37
src/App/Osu/SongBrowser/SongBrowser.cpp

@@ -895,6 +895,43 @@ void SongBrowser::drawSelectedBeatmapBackgroundImage(Graphics *g, float alpha) {
     }
 }
 
+bool SongBrowser::selectBeatmapset(i32 set_id) {
+    auto beatmapset = getDatabase()->getBeatmapSet(set_id);
+    if(beatmapset == NULL) {
+        // Pasted from Downloader::download_beatmap
+        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);
+
+        beatmapset = getDatabase()->getBeatmapSet(set_autodl);
+    }
+
+    if(beatmapset == NULL) {
+        return false;
+    }
+
+    // Just picking the hardest diff for now
+    DatabaseBeatmap *best_diff = NULL;
+    const std::vector<DatabaseBeatmap *> &diffs = beatmapset->getDifficulties();
+    for(size_t d = 0; d < diffs.size(); d++) {
+        DatabaseBeatmap *diff = diffs[d];
+        if(!best_diff || diff->getStarsNomod() > best_diff->getStarsNomod()) {
+            best_diff = diff;
+        }
+    }
+
+    if(best_diff == NULL) {
+        osu->getNotificationOverlay()->addNotification("Beatmapset has no difficulties :/");
+        return false;
+    } else {
+        osu->m_songBrowser2->onDifficultySelected(best_diff, false);
+        osu->m_songBrowser2->selectSelectedBeatmapSongButton();
+        return true;
+    }
+}
+
 void SongBrowser::mouse_update(bool *propagate_clicks) {
     if(!m_bVisible) return;
     ScreenBackable::mouse_update(propagate_clicks);
@@ -948,8 +985,10 @@ void SongBrowser::mouse_update(bool *propagate_clicks) {
             set_autodl = 0;
         }
     } else if(set_autodl) {
-        auto beatmapset = getDatabase()->getBeatmapSet(set_autodl);
-        if(beatmapset == NULL) {
+        if(selectBeatmapset(set_autodl)) {
+            map_autodl = 0;
+            set_autodl = 0;
+        } else {
             float progress = -1.f;
             download_beatmapset(set_autodl, &progress);
             if(progress == -1.f) {
@@ -962,37 +1001,11 @@ void SongBrowser::mouse_update(bool *propagate_clicks) {
                 auto text = UString::format("Downloading... %.2f%%", progress * 100.f);
                 osu->getNotificationOverlay()->addNotification(text);
             } else {
-                // Pasted from Downloader::download_beatmap
-                auto mapset_path = UString::format(MCENGINE_DATA_DIR "maps/%d/", set_autodl);
-                // 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_autodl);
-
-                beatmapset = getDatabase()->getBeatmapSet(set_autodl);
-            }
-        }
+                selectBeatmapset(set_autodl);
 
-        if(beatmapset != NULL) {
-            // Just picking the hardest diff for now
-            DatabaseBeatmap *best_diff = NULL;
-            const std::vector<DatabaseBeatmap *> &diffs = beatmapset->getDifficulties();
-            for(size_t d = 0; d < diffs.size(); d++) {
-                DatabaseBeatmap *diff = diffs[d];
-                if(!best_diff || diff->getStarsNomod() > best_diff->getStarsNomod()) {
-                    best_diff = diff;
-                }
-            }
-
-            if(best_diff == NULL) {
-                osu->getNotificationOverlay()->addNotification("Beatmapset has no difficulties :/");
-            } else {
-                osu->m_songBrowser2->onDifficultySelected(best_diff, false);
-                osu->m_songBrowser2->selectSelectedBeatmapSongButton();
+                map_autodl = 0;
+                set_autodl = 0;
             }
-
-            map_autodl = 0;
-            set_autodl = 0;
         }
     }
 
@@ -1594,6 +1607,11 @@ void SongBrowser::refreshBeatmaps() {
         first_refresh = false;
     }
 
+    auto diff2 = m_selectedBeatmap->getSelectedDifficulty2();
+    if(diff2) {
+        beatmap_to_reselect_after_db_load = diff2->getMD5Hash();
+    }
+
     m_selectedBeatmap->pausePreviewMusic();
     m_selectedBeatmap->deselect();
     SAFE_DELETE(m_selectedBeatmap);
@@ -2983,12 +3001,10 @@ void SongBrowser::onDatabaseLoadingFinished() {
 
     if(osu_songbrowser_search_hardcoded_filter.getString().length() > 0) onSearchUpdate();
 
-    // main menu starts playing a song before the database is loaded,
-    // re-select it after the database has been loaded
-    if(osu->m_mainMenu->preloaded_beatmapset != NULL) {
-        auto matching_beatmap = getDatabase()->getBeatmapDifficulty(osu->m_mainMenu->preloaded_beatmap->getMD5Hash());
-        if(matching_beatmap) {
-            onDifficultySelected(matching_beatmap, false);
+    if(beatmap_to_reselect_after_db_load.hash[0] != 0) {
+        auto beatmap = getDatabase()->getBeatmapDifficulty(beatmap_to_reselect_after_db_load);
+        if(beatmap) {
+            onDifficultySelected(beatmap, false);
             selectSelectedBeatmapSongButton();
         }
 

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

@@ -101,6 +101,7 @@ class SongBrowser : public ScreenBackable {
 
     virtual CBaseUIContainer *setVisible(bool visible);
 
+    bool selectBeatmapset(i32 set_id);
     void selectSelectedBeatmapSongButton();
     void onPlayEnd(bool quit = true);  // called when a beatmap is finished playing (or the player quit)
 
@@ -330,6 +331,7 @@ class SongBrowser : public ScreenBackable {
     std::unordered_map<MD5Hash, SongButton *> hashToSongButton;
     bool m_bBeatmapRefreshScheduled;
     UString m_sLastOsuFolder;
+    MD5Hash beatmap_to_reselect_after_db_load;
 
     // keys
     bool m_bF1Pressed;

+ 35 - 1
src/Engine/Main/main_Windows.cpp

@@ -7,8 +7,14 @@
 #include <shellapi.h>
 // clang-format on
 
+#include "Database.h"
+#include "DatabaseBeatmap.h"
+#include "Downloader.h"  // for extract_beatmapset
+#include "File.h"
+#include "MainMenu.h"
 #include "OptionsMenu.h"
 #include "Osu.h"
+#include "SongBrowser/SongBrowser.h"
 #include "Skin.h"
 
 // NEXTRAWINPUTBLOCK macro requires this
@@ -187,7 +193,35 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
 
                 if(utf8filepath.length() < 4) continue;
                 auto extension = env->getFileExtensionFromFilePath(utf8filepath);
-                if(!extension.compare("osk") || !extension.compare("zip")) {
+                if(!extension.compare("osz")) {
+                    File osz(utf8filepath);
+                    i32 set_id = extract_beatmapset_id(osz.readFile(), osz.getFileSize());
+                    if(set_id == -1) {
+                        osu->getNotificationOverlay()->addNotification("Beatmapset doesn't have a valid ID.");
+                        continue;
+                    }
+
+                    std::string mapset_dir = MCENGINE_DATA_DIR "maps\\";
+                    mapset_dir.append(std::to_string(set_id));
+                    mapset_dir.append("\\");
+                    if(!env->directoryExists(mapset_dir)) {
+                        env->createDirectory(mapset_dir);
+                    }
+                    if(!extract_beatmapset(osz.readFile(), osz.getFileSize(), mapset_dir)) {
+                        osu->getNotificationOverlay()->addNotification("Failed to extract beatmapset");
+                        continue;
+                    }
+
+                    osu->getSongBrowser()->getDatabase()->addBeatmap(mapset_dir);
+                    if(!osu->getSongBrowser()->selectBeatmapset(set_id)) {
+                        osu->getNotificationOverlay()->addNotification("Failed to import beatmapset");
+                        continue;
+                    }
+
+                    // prevent song browser from picking main menu song after database loads
+                    // (we just loaded and selected another song, so previous no longer applies)
+                    SAFE_DELETE(osu->m_mainMenu->preloaded_beatmapset);
+                } else if(!extension.compare("osk") || !extension.compare("zip")) {
                     Skin::unpack(utf8filepath.c_str());
                     if(first_skin.length() == 0) {
                         first_skin = utf8filepath;